diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 0fdb8c379..9ede3a352 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -7,13 +7,17 @@ on: push: branches: - 'dev' + - 'release/*' + +permissions: + contents: read jobs: - testrun_package: + create_package: permissions: {} name: Package runs-on: ubuntu-22.04 - timeout-minutes: 5 + timeout-minutes: 10 steps: - name: Checkout source uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -24,4 +28,72 @@ jobs: uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: testrun_package - path: testrun*.deb \ No newline at end of file + path: testrun*.deb + + install_package_22: + permissions: {} + needs: create_package + name: Install on Ubuntu 22.04 + runs-on: ubuntu-22.04 + timeout-minutes: 15 + steps: + - name: Checkout source + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Download package + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: testrun_package + - name: Install dependencies + shell: bash {0} + run: sudo cmd/prepare + - name: Install package + shell: bash {0} + run: sudo apt install ./testrun*.deb + - name: Start testrun + shell: bash {0} + run: sudo testrun > >(tee testrun_output.log) 2>&1 & + - name: Verify testrun started + shell: bash {0} + run: | + sleep 5 + if grep -q "API waiting for requests" testrun_output.log; then + echo "Testrun started successfully." + else + echo "Testrun did not start correctly." + cat testrun_output.log + exit 1 + fi + + install_package_24: + permissions: {} + needs: create_package + name: Install on Ubuntu 24.04 + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Checkout source + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Download package + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: testrun_package + - name: Install dependencies + shell: bash {0} + run: sudo cmd/prepare + - name: Install package + shell: bash {0} + run: sudo apt install ./testrun*.deb + - name: Start testrun + shell: bash {0} + run: sudo testrun > >(tee testrun_output.log) 2>&1 & + - name: Verify testrun started + shell: bash {0} + run: | + sleep 5 + if grep -q "API waiting for requests" testrun_output.log; then + echo "Testrun started successfully." + else + echo "Testrun did not start correctly." + cat testrun_output.log + exit 1 + fi diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 12884c718..f0f89a631 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -7,10 +7,6 @@ on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: - # To guarantee Maintained check is occasionally updated. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained - schedule: - - cron: '20 6 * * 4' push: branches: [ "main" ] @@ -70,4 +66,4 @@ jobs: - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 with: - sarif_file: results.sarif + sarif_file: results.sarif \ No newline at end of file diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 0556a2189..d8da3ada0 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -7,10 +7,29 @@ on: - cron: '0 13 * * *' jobs: + testrun_tests: + permissions: {} + name: Tests + runs-on: ubuntu-22.04 + timeout-minutes: 30 + steps: + - name: Checkout source + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Install dependencies + shell: bash {0} + run: cmd/prepare + - name: Install Testrun + shell: bash {0} + run: TESTRUN_DIR=. cmd/install + timeout-minutes: 30 + - name: Run tests + shell: bash {0} + run: testing/tests/test_tests + testrun_baseline: permissions: {} name: Baseline - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 timeout-minutes: 20 steps: - name: Checkout source @@ -29,7 +48,7 @@ jobs: testrun_api: permissions: {} name: API - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 timeout-minutes: 60 steps: - name: Checkout source @@ -39,7 +58,7 @@ jobs: run: cmd/prepare - name: Install Testrun shell: bash {0} - run: TESTRUN_DIR=. cmd/install + run: cmd/install -l timeout-minutes: 30 - name: Run tests shell: bash {0} @@ -55,6 +74,52 @@ jobs: name: runtime_api_${{ github.run_id }} path: runtime.tgz + testrun_unit: + permissions: {} + name: Unit + runs-on: ubuntu-22.04 + timeout-minutes: 15 + steps: + - name: Checkout source + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Install dependencies + shell: bash {0} + run: cmd/prepare + - name: Install Testrun + shell: bash {0} + run: cmd/install -l + - name: Run tests for conn module + shell: bash {0} + run: bash testing/unit/run_test_module.sh conn captures ethtool ifconfig output + - name: Run tests for dns module + shell: bash {0} + run: bash testing/unit/run_test_module.sh dns captures reports output + - name: Run tests for ntp module + shell: bash {0} + run: bash testing/unit/run_test_module.sh ntp captures reports output + - name: Run tests for protocol module + shell: bash {0} + run: bash testing/unit/run_test_module.sh protocol captures output + - name: Run tests for services module + shell: bash {0} + run: bash testing/unit/run_test_module.sh services reports results output + - name: Run tests for tls module + shell: bash {0} + run: bash testing/unit/run_test_module.sh tls captures certAuth certs reports root_certs output + - name: Run tests for risk profiles + shell: bash {0} + run: bash testing/unit/run_report_test.sh testing/unit/risk_profile/risk_profile_test.py + - name: Run tests for reports + shell: bash {0} + run: bash testing/unit/run_report_test.sh testing/unit/report/report_test.py + - name: Upload reports + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 + if: ${{ always() }} + with: + if-no-files-found: error + name: unit_reports_${{ github.run_id }} + path: testing/unit/report/output + pylint: permissions: {} name: Pylint @@ -76,7 +141,7 @@ jobs: - name: Install Node uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: - node-version: 18.18.0 + node-version: 18.19.0 - name: Install Chromium Browser run: sudo apt install chromium-browser - name: Install dependencies @@ -85,7 +150,7 @@ jobs: - name: Run tests run: | export CHROME_BIN=/usr/bin/chromium-browser - CI=true npm run test-headless + CI=true npm run test-ci env: CI: true working-directory: ./modules/ui @@ -99,7 +164,7 @@ jobs: - name: Install Node uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: - node-version: 18.18.0 + node-version: 18.19.0 - name: Install dependencies run: npm install && npm ci working-directory: ./modules/ui diff --git a/.gitignore b/.gitignore index 82b6bbf64..f1c4ea203 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ build/ # Ignore generated files from unit tests testing/unit_test/temp/ +testing/unit/conn/output/ testing/unit/dns/output/ testing/unit/nmap/output/ testing/unit/ntp/output/ @@ -15,6 +16,12 @@ testing/unit/tls/output/ testing/unit/tls/tmp/ testing/unit/report/output/ testing/unit/risk_profile/output/ +testing/unit/services/output/ + +# Ignore generated files from requirements generation +*requirements_freeze.txt +*unique_freeze.txt +*requirements_gen.txt *.deb make/DEBIAN/postinst diff --git a/README.md b/README.md index 4a04e8885..e6cfc8943 100644 --- a/README.md +++ b/README.md @@ -4,84 +4,93 @@ [![CodeQL](https://github.com/google/testrun/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)](https://github.com/google/testrun/actions/workflows/github-code-scanning/codeql) [![Testrun test suite](https://github.com/google/testrun/actions/workflows/testing.yml/badge.svg?branch=main&event=push)](https://github.com/google/testrun/actions/workflows/testing.yml) -## Introduction :wave: -Testrun automates specific test cases to verify network and security functionality in IoT devices. It is an open source tool which allows manufacturers of IP capable devices to test their devices for the purposes of Device Qualification within the BOS program. +# Introduction :wave: -## Motivation :bulb: -Without tools like Testrun, test labs and engineers may need to maintain a large and complex network coupled with dynamic configuration files and constant software updates. The major issues which can and should be solved are: - 1) The complexity of managing a testing network - 2) The time required to perform testing of network functionality - 3) The accuracy and consistency of testing network functionality +Testrun automates specific test cases to verify network and security functionality in IoT devices. It's an open-source tool that manufacturers use to test their IP-capable devices for the purpose of device qualification within Google's Building Operating System (BOS) program. -## How it works :triangular_ruler: -Testrun creates an isolated and controlled network environment on a linux machine. This removes the necessity for complex hardware, advanced knowledge and networking experience whilst enabling test engineers to validate device behaviour against Google’s Building Operating System requirements. +# Motivation :bulb: -Two modes are supported by Testrun: +Test labs and engineers often need to maintain a large and complex network coupled with dynamic configuration files and constant software updates. Testrun helps address major issues like: -
- - Automated testing - +- The complexity of managing a testing network +- The time required to perform testing of network functionality +- The accuracy and consistency of testing network functionality -Once the device has become operational (steady state), automated testing of the DUT (device under test) will begin. Containerized test modules will then execute against the device, one module at a time. Once all test modules have been executed, a report will be produced - presenting the results. -
+# How it works :triangular_ruler: -
+Testrun creates an isolated and controlled network environment on a Linux machine. This removes the necessity for complex hardware, advanced knowledge, and networking experience while enabling test engineers to validate device behavior against Google's BOS requirements. - - Lab network - +Testrun supports two modes: automated testing and lab network. -When manual testing or configuration changes are required, Testrun will provide the network and some tools to assist an engineer performing the additional testing. This reduces the need to maintain a separate but identical lab network. Testrun will take care of packet captures and logs for each network service for further debugging. +## Automated testing -
+Automated testing of the device under test (DUT) begins once the device is operational (steady state). Containerized test modules execute against the device one module at a time. Testrun produces a report with the results after all modules are executed. -## Minimum requirements :computer: -### Hardware - - PC running Ubuntu LTS 20.04, 22.04 or 24.04 (laptop or desktop) - - 2x USB ethernet adapter (One may be built in ethernet) - - Internet connection -### Software -- Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) -### Device under test (DUT) - - DHCP client - The device must be able to obtain an IP address via DHCP +## Lab network -## Get started ▶️ -Once you have met the hardware and software requirements, you can get started with Testrun by following the [Get started guide](docs/get_started.md). +Testrun provides the network and assistive tools for engineers when manual testing or configuration changes are required, reducing the need to maintain a separate but identical lab network. Testrun handles packet captures and logs for each network service for further debugging. -## Roadmap :chart_with_upwards_trend: -Testrun will constantly evolve to further support end-users by automating device network behaviour against industry standards. For further information on upcoming features, check out the [Roadmap](docs/roadmap.pdf). +# Minimum requirements :computer: -## Accessibility :busts_in_silhouette: -We are proud of our tool and strive to provide an enjoyable experience for all of our users. Testrun goes through rigorous accessibility testing at each release. You can read more about [Google and Accessibility here](https://www.google.co.uk/accessibility). You are welcome to submit a new issue and provide feedback on our implementations. To find out how Testrun implements accessibility features, you can view a [short video here](docs/ui/accessibility.mp4). +## Hardware -## Issue reporting :triangular_flag_on_post: -If the application has come across a problem at any point during setup or use, please raise an issue under the [issues tab](https://github.com/google/testrun/issues). Issue templates exist for both bug reports and feature requests. If neither of these are appropriate for your issue, raise a blank issue instead. +- PC running Ubuntu LTS 22.04 or 24.04 (laptop or desktop) +- 2x ethernet ports (USB ethernet adapters work too) +- Internet connection -## Contributing :keyboard: -The contributing requirements can be found in [CONTRIBUTING.md](CONTRIBUTING.md). In short, checkout the [Google CLA](https://cla.developers.google.com/) site to get started. +## Software -## FAQ :raising_hand: -1) I have an issue whilst installing/upgrading Testrun, what do I do? +Testrun requires Docker. Refer to the [installation guide](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) for more information. - Sometimes, issues may arise when installing or upgrading Testrun - this may happen due to one of many reasons due to the nature of the application. However, most of the time, it can be resolved by following a full Testrun re-install by using these commands: - - ```sudo docker system prune -a``` - - ```sudo apt install ./testrun-*.deb``` +## Device under test (DUT) -2) What device networking functionality is validated by Testrun? +The DUT must be able to obtain an IP address via DHCP. - Best practices and requirements for IoT devices are constantly changing due to technological advances and discovery of vulnerabilities. - The current expectations for IoT devices on Google deployments can be found in the [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements). - Testrun aims to automate as much of the Application Security Requirements as possible. +# Get started :arrow_forward: -3) What services are provided on the virtual network? +Once you meet the hardware and software requirements, follow the Testrun [Get started guide](/docs/get_started.md). Additional guidance is available in the [docs directory](/docs). - The following are network services that are containerized and accessible to the device under test though are likely to change over time: - - DHCP in failover configuration with internet connectivity - - IPv6 SLAAC - - DNS - - NTPv4 +# Roadmap :chart_with_upwards_trend: -4) Can I run Testrun on a virtual machine? +Testrun continually evolves to further support end users by automating device network behavior against industry standards. For information on upcoming features, check out the [Roadmap](/docs/roadmap.pdf). - Testrun can be virtualized if the 2x ethernet adapters are passed through to a VirtualBox VM as a USB device rather than managed network adapters. You can view the guide to working on a [virtual machine here](docs/virtual_machine.md). +# Accessibility :busts_in_silhouette: + +We're proud of our tool and strive to provide an enjoyable experience for everyone. Testrun goes through rigorous accessibility testing at each release. Download the [Testrun: Accessible features](https://github.com/google/testrun/raw/refs/heads/main/docs/ui/accessibility.mp4) video to learn more.You're welcome to [submit a new issue](https://github.com/google/testrun/issues) and provide feedback on our implementations. To learn more about Google's [Belonging initiative](https://www.google.co.uk/accessibility) and their approach to accessibility, visit their site. + +# Issue reporting :triangular_flag_on_post: + +If you encounter a problem during setup or use, raise an issue under the [Issues tab](https://github.com/google/testrun/issues). Issue templates exist for both bug reports and feature requests. If neither of these apply, raise a blank issue instead. + +# Contributing :keyboard: + +We strongly encourage contributions from the community. Review the requirements on the ["How to Contribute" page](CONTRIBUTING.md), then follow the [developer guidelines](/docs/dev/README.md). + +# FAQ :raising_hand: + +#### 1. What should I do if I have an issue while installing or upgrading Testrun? + + You can resolve most issues by reinstalling Testrun using these commands: +- `sudo docker system prune -a` +- `sudo apt install ./testrun*.deb` + +If this doesn't resolve the problem, [raise an issue](https://github.com/google/testrun/issues). + +#### 2. What device networking functionality does Testrun validate? + +Best practices and requirements for IoT devices change often due to technological advances and discovery of vulnerabilities. You can find the current expectations for IoT devices on Google deployments in the [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements). Testrun aims to automate as much of the Application Security Requirements as possible. + +#### 3. What services are provided on the virtual network? + +The following network services are containerized and accessible to the DUT: + +- DHCP in failover configuration with internet connectivity +- IPv6 SLAAC +- DNS +- NTPv4 + +Note that this list is likely to change over time. + +#### 4. Can I run Testrun on a virtual machine? + +Testrun can be virtualized if the 2x Ethernet adapters are passed through to a VirtualBox VM as a USB device rather than managed network adapters. Visit the [virtual machine guide](/docs/virtual_machine.md) for additional details. \ No newline at end of file diff --git a/cmd/build b/cmd/build index d15171f31..5d04e049b 100755 --- a/cmd/build +++ b/cmd/build @@ -36,53 +36,63 @@ fi # Builds all docker images echo Building docker images -# Build user interface -echo Building user interface -if docker build -t test-run/ui -f modules/ui/ui.Dockerfile . ; then +# Check if UI has already been built (if -l was used during install) +if [ ! -d "modules/ui/dist" ]; then + cmd/build_ui +fi + +# Build UI image +if docker build -t testrun/ui -f modules/ui/ui.Dockerfile . ; then echo Successully built the user interface else - echo An error occured whilst building the user interface + echo An error occurred whilst building the user interface + exit 1 +fi + +# Build websockets server +echo Building websockets server +if docker build -t testrun/ws -f modules/ws/ws.Dockerfile . ; then + echo Successully built the web sockets server +else + echo An error occurred whilst building the websockets server exit 1 fi # Build network modules echo Building network modules -mkdir -p build/network for dir in modules/network/* ; do module=$(basename $dir) echo Building network module $module... - if docker build -f modules/network/$module/$module.Dockerfile -t test-run/$module . ; then + if docker build -f modules/network/$module/$module.Dockerfile -t testrun/$module . ; then echo Successfully built container for network $module else - echo An error occured whilst building container for network module $module + echo An error occurred whilst building container for network module $module exit 1 fi done # Build validators echo Building network validators -mkdir -p build/devices for dir in modules/devices/* ; do module=$(basename $dir) echo Building validator module $module... - if docker build -f modules/devices/$module/$module.Dockerfile -t test-run/$module . ; then + if docker build -f modules/devices/$module/$module.Dockerfile -t testrun/$module . ; then echo Successfully built container for device module $module else - echo An error occured whilst building container for device module $module + echo An error occurred whilst building container for device module $module exit 1 fi done # Build test modules echo Building test modules -mkdir -p build/test for dir in modules/test/* ; do module=$(basename $dir) echo Building test module $module... - if docker build -f modules/test/$module/$module.Dockerfile -t test-run/$module-test . ; then + if docker build -f modules/test/$module/$module.Dockerfile -t testrun/$module-test . ; then echo Successfully built container for test module $module else - echo An error occured whilst building container for test module $module + echo An error occurred whilst building container for test module $module exit 1 fi done diff --git a/cmd/build_ui b/cmd/build_ui new file mode 100755 index 000000000..bbfc53764 --- /dev/null +++ b/cmd/build_ui @@ -0,0 +1,37 @@ +#!/bin/bash -e + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build the UI +echo Building the ui builder + +# Build UI builder image +if docker build -t testrun/build-ui -f modules/ui/build.Dockerfile . ; then + echo Successully built the ui builder +else + echo An error occurred whilst building the ui builder + exit 1 +fi + +# Check that the container is not already running +docker kill tr-ui-build 2> /dev/null || true + +echo "Building the user interface" + +# Start build container and build the ui dist +docker run --rm -v "$(pwd)"/modules/ui:/modules/ui testrun/build-ui /bin/sh -c "npm install && npm run build" + +# Kill the container (Should not be running anymore) +docker kill tr-ui-build 2> /dev/null || true diff --git a/cmd/install b/cmd/install index 53d12b324..baf0f5469 100755 --- a/cmd/install +++ b/cmd/install @@ -20,15 +20,29 @@ echo Installing application dependencies while getopts ":l" option; do case $option in l) # Install Testrun in local directory - TESTRUN_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")"/.. && pwd) + TESTRUN_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")"/.. && pwd) esac done # Check if TESTRUN_DIR has been set, otherwise install in /usr/local/testrun if [[ -z "${TESTRUN_DIR}" ]]; then TESTRUN_DIR=/usr/local/testrun + + # Check that user is sudo + if [[ "$EUID" -ne 0 ]]; then + echo "Installing Testrun in the default location requires sudo. Run using sudo cmd/install" + exit 1 + fi + else TESTRUN_DIR="${TESTRUN_DIR}" + + # Check that user is in docker group + if ! (id -nGz "$USER" | grep -qzxF "docker"); then + echo User is not in docker group. Follow https://docs.docker.com/engine/install/linux-postinstall/ to finish setting up docker. + exit 1 + fi + fi echo Installing Testrun at $TESTRUN_DIR @@ -51,18 +65,16 @@ cp -n local/system.json.example local/system.json deactivate # Build docker images -sudo cmd/build +cmd/build # Create local folders -mkdir -p local/devices -mkdir -p local/root_certs -mkdir -p local/risk_profiles +mkdir -p local/{devices,root_certs,risk_profiles} # Set file permissions on local # This does not work on GitHub actions if logname ; then USER_NAME=$(logname) - sudo chown -R "$USER_NAME" local + sudo chown -R "$USER_NAME" local resources || true fi echo Finished installing Testrun diff --git a/cmd/package b/cmd/package index fc418ab05..8897e947e 100755 --- a/cmd/package +++ b/cmd/package @@ -16,6 +16,18 @@ # Creates a package for Testrun +# Check that user is not root +if [[ "$EUID" == 0 ]]; then + echo "Must not run as root. Use cmd/package as regular user" + exit 1 +fi + +# Check that user is in docker group +if ! (id -nGz "$USER" | grep -qzxF "docker"); then + echo User is not in docker group. Follow https://docs.docker.com/engine/install/linux-postinstall/ to finish setting up docker. + exit 1 +fi + MAKE_SRC_DIR=make MAKE_CONTROL_DIR=make/DEBIAN/control @@ -25,10 +37,10 @@ version=$(grep -R "Version: " $MAKE_CONTROL_DIR | awk '{print $2}') # Replace invalid characters version="${version//./_}" -# Delete existing make files -rm -rf $MAKE_SRC_DIR/usr +echo Building package for testrun v${version} # Delete existing make files +echo Cleaning up previous build files rm -rf $MAKE_SRC_DIR/usr # Copy testrun script to /bin @@ -60,6 +72,9 @@ mkdir -p $MAKE_SRC_DIR/usr/local/testrun/local/risk_profiles mkdir -p local/root_certs cp -r local/root_certs $MAKE_SRC_DIR/usr/local/testrun/local/ +# Build the UI +cmd/build_ui + # Copy framework and modules into testrun folder cp -r {framework,modules} $MAKE_SRC_DIR/usr/local/testrun diff --git a/cmd/prune b/cmd/prune index 9f471897d..4c2796460 100755 --- a/cmd/prune +++ b/cmd/prune @@ -25,17 +25,16 @@ fi # Remove docker images echo Removing docker images -docker_images=$(sudo docker images --filter=reference="test-run/*" -q) +docker_images=$(sudo docker images --filter=reference="testrun/*" -q) if [ -z "$docker_images" ]; then echo No docker images to delete else - sudo docker rmi $docker_images > /dev/null + sudo docker rmi $docker_images fi # Remove docker networks echo Removing docker networks -sudo docker network rm endev0 > /dev/null -# Private network not used, add cleanup -# back in if/when implemented -#sudo docker network rm tr-private-net > /dev/null \ No newline at end of file +sudo docker network rm endev0 || true + +echo Successfully pruned Testrun resources diff --git a/cmd/update_requirements b/cmd/update_requirements new file mode 100755 index 000000000..f185f87d9 --- /dev/null +++ b/cmd/update_requirements @@ -0,0 +1,115 @@ +# #!/bin/bash -e + +# # Copyright 2023 Google LLC +# # +# # Licensed under the Apache License, Version 2.0 (the "License"); +# # you may not use this file except in compliance with the License. +# # You may obtain a copy of the License at +# # +# # https://www.apache.org/licenses/LICENSE-2.0 +# # +# # Unless required by applicable law or agreed to in writing, software +# # distributed under the License is distributed on an "AS IS" BASIS, +# # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# # See the License for the specific language governing permissions and +# # limitations under the License. + +# update_requirements(){ +# modules_dir="$1" +# for dir in $modules_dir/* ; do +# echo "dir: $dir" +# module=$(basename "$dir") +# MODULE_DIR="$PWD/$modules_dir/$module/python" +# IMAGE=testrun/$module +# # Check if updating the test modules +# if [[ $modules_dir == *test* ]]; then +# # Append '-test' to the variable +# IMAGE="${IMAGE}-test" +# fi +# echo "Module dir: $MODULE_DIR" +# echo "Image: $IMAGE" +# echo Updating requirements for module $modules_dir/$module... + +# if [ -e "$MODULE_DIR/requirements.txt" ]; then +# if docker run --rm -v "$PWD/$modules_dir/$module/python/:/testrun/python/" --entrypoint /bin/bash $IMAGE -c "pip3 freeze > /testrun/python/requirements_freeze.txt" ; then +# echo Successfully built requirements file for module $modules_dir/$module + +# # Normalize line endings and remove extra spaces +# dos2unix "$MODULE_DIR/requirements.txt" "$MODULE_DIR/requirements_freeze.txt" +# sed -i 's/^[ \t]*//;s/[ \t]*$//' "$MODULE_DIR/requirements.txt" "$MODULE_DIR/requirements_freeze.txt" + +# # Temporary file to store unique packages +# > "$MODULE_DIR/unique_freeze.txt" + +# # Find unique packages in requirements_freeze.txt that are not in requirements.txt +# while IFS= read -r freeze_line; do +# # Extract the package name from freeze_line +# freeze_package=$(echo "$freeze_line" | cut -d'=' -f1 | xargs) + +# echo "Frozen package: $freeze_package" +# # Search for the package name in requirements.txt, ignoring case and whitespace +# if ! grep -iq "^${freeze_package}$" "$MODULE_DIR/requirements.txt"; then +# echo "$freeze_line" >> "$MODULE_DIR/unique_freeze.txt" +# fi +# done < "$MODULE_DIR/requirements_freeze.txt" + +# # Temporary file to store generated requirements +# > "$MODULE_DIR/requirements_gen.txt" + +# # Add the downstream packages at the top of requirements_gen.txt +# # so we pull in package dependencies before the defined dependency +# # to prevent auto-upgrades in the package pipeline +# echo "# Dependencies to user defined packages" > "$MODULE_DIR/requirements_gen.txt" +# echo -e "# Package dependencies should always be defined before the user defined" >> "$MODULE_DIR/requirements_gen.txt" +# echo -e "# packages to prevent auto-upgrades of stable dependencies" >> "$MODULE_DIR/requirements_gen.txt" +# cat "$MODULE_DIR/unique_freeze.txt" >> "$MODULE_DIR/requirements_gen.txt" + +# # Create the requirements_gen.txt file +# echo -e "\n# User defined packages" >> "$MODULE_DIR/requirements_gen.txt" + +# # Loop through each package in requirements.txt +# while IFS= read -r package; do +# # Trim leading and trailing whitespace from package +# package=$(echo "$package" | xargs) +# echo "Package: $package" + +# # Extract the base package name (without version or any comparison operator) +# base_package=$(echo "$package" | sed -E 's/[<>=!].*//') +# echo "Base package: $base_package" + +# # Check if the base package is a comment and non-empty +# if [[ -n "$base_package" && "$base_package" != \#* ]]; then +# # Perform the grep only if it is not a comment +# versioned_package=$(grep -i "^${base_package}==" "$MODULE_DIR/requirements_freeze.txt") +# else +# # Set versioned_package to empty if it's a comment +# versioned_package="" +# fi + +# # Debug output: Print the result of matching +# echo "Versioned Package: '$versioned_package'" + +# if [ -n "$versioned_package" ]; then +# # If the package with the version is found, add it to requirements_gen.txt +# echo "$versioned_package" >> "$MODULE_DIR/requirements_gen.txt" +# else +# # If not found, just add the package as is from requirements.txt +# echo "$package" >> "$MODULE_DIR/requirements_gen.txt" +# fi +# done < "$MODULE_DIR/requirements.txt" +# echo "Module done" + +# else +# echo An error occurred while building requirements file for network module $module +# exit 1 +# fi +# else +# echo No requirements.txt file defined for this module +# fi +# done +# } +# echo Updating python requirements in network modules +# update_requirements modules/network + +# echo Updating python requirements in test modules +# update_requirements modules/test diff --git a/docs/README.md b/docs/README.md index 96eb32223..efea0d413 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,18 +1,19 @@ Testrun logo +# Contents -## Contents +- [Get started](/docs/get_started.md) + - [Run on a virtual machine](/docs/virtual_machine.md) +- [Network](/docs/network/README.md) + - [Network addresses](/docs/network/addresses.md) + - [Add a new network service](/docs/network/add_new_service.md) +- [Testing](/docs/test/README.md) + - [Test modules](/docs/test/modules.md) + - [Test results](/docs/test/statuses.md) +- [Developer guidelines](/docs/dev/README.md) +- [Accessibility](/docs/ui/accessibility.md) +- [Roadmap](/docs/roadmap.pdf) - - [Get Started](get_started.md) - - [Network](network/README.md) - - [Network Overview](network/README.md) - - [How to identify network interfaces](network/identify_interfaces.md) - - [Addresses](network/addresses.md) - - [Add a new network service](network/add_new_service.md) - - [Testing](test/README.md) - - [Test modules](test/modules.md) - - [Test statuses](test/statuses.md) - - [Development](dev/README.md) - - [Running on a virtual machine](virtual_machine.md) - - [Accessibility](ui/accessibility.mp4) - - [Roadmap](roadmap.pdf) +# Something missing? + +To request additional documentation or report an issue with existing resources, visit [the Issues tab](https://github.com/google/testrun/issues/new/choose). diff --git a/docs/additional_config.md b/docs/additional_config.md new file mode 100644 index 000000000..ea9aaaf51 --- /dev/null +++ b/docs/additional_config.md @@ -0,0 +1,121 @@ +# Additional Configuration Options + +Some configuration options are available but not exposed through the user interface and requires direct access. +Modification of various configuration files is necessary to access these options. + +## Override test module timeout at the system level + +Testrun attempts to set reasonable timeouts for test modules to prevent overly long test times but sometimes +a device or series of device may require longer than these default values. These can be overridden at +the test module configuration level but is not preferred since these changes will be undone during every +version upgrade. To modify these values: + +1. Navigate to the testrun installation directory. By default, this will be at: + `/usr/local/testrun` + +2. Open the system.json file and add the following section: + `"test_modules":{}` + +3. Add the module name(s) and timeout property into this test_modules section you wish to +set the timeout property for: + ``` + "test_modules":{ + "connection":{ + "timeout": 500 + } + } + ``` + +Before timeout options: +``` +{ + "network": { + "device_intf": "ens0", + "internet_intf": "ens1" + }, + "log_level": "DEBUG", + "startup_timeout": 60, + "monitor_period": 60, + "max_device_reports": 5, + "org_name": "", + "single_intf": false + } +``` + +After timeout options: +``` +{ + "network": { + "device_intf": "ens0", + "internet_intf": "ens1" + }, + "log_level": "DEBUG", + "startup_timeout": 60, + "monitor_period": 60, + "max_device_reports": 5, + "org_name": "", + "single_intf": false, + "test_modules":{ + "connection":{ + "timeout": 500 + } + } +} +``` + +## Override test module log level at the system level + +Test modules default to the log level info to prevent unecessary logging. These can be overridden at the test module configuration level but is not preferred since these changes will be undone during every version upgrade. To modify these values: + +1. Navigate to the testrun installation directory. By default, this will be at: + `/usr/local/testrun` + +2. Open the system.json file and add the following section: + `"test_modules":{}` + +3. Add the module name(s) and log_level property into this test_modules section you wish to +set the log_level property for: + ``` + "test_modules":{ + "connection":{ + "log_level": "DEGUG" + } + } + ``` +Valid options for modifying the log level are: INFO, DEBUG, WARNING, ERROR. + +Before log_level options: +``` +{ + "network": { + "device_intf": "ens0", + "internet_intf": "ens1" + }, + "log_level": "DEBUG", + "startup_timeout": 60, + "monitor_period": 60, + "max_device_reports": 5, + "org_name": "", + "single_intf": false + } +``` + +After log_level options: +``` +{ + "network": { + "device_intf": "ens0", + "internet_intf": "ens1" + }, + "log_level": "DEBUG", + "startup_timeout": 60, + "monitor_period": 60, + "max_device_reports": 5, + "org_name": "", + "single_intf": false, + "test_modules":{ + "connection":{ + "log_level": "DEBUG" + } + } +``` \ No newline at end of file diff --git a/docs/configure_device.md b/docs/configure_device.md deleted file mode 100644 index 1db7155be..000000000 --- a/docs/configure_device.md +++ /dev/null @@ -1,31 +0,0 @@ -Testrun logo - -## Device Configuration (Deprecated) - -The device configuration file allows you to customize the testing behavior for a specific device. This file is located at `local/devices/{Device Name}/device_config.json`. Below is an overview of how to configure the device tests. - -## Device Information - -The device information section includes the manufacturer, model, and MAC address of the device. These details help identify the specific device being tested. - -## Test Modules - -Test modules are groups of tests that can be enabled or disabled as needed. You can choose which test modules to run on your device. - -### Enabling and Disabling Test Modules - -To enable or disable a test module, modify the `enabled` field within the respective module. Setting it to `true` enables the module, while setting it to `false` disables the module. - -## Customizing the Device Configuration - -To customize the device configuration for your specific device, follow these steps: - -1. Copy the default configuration file provided in the `resources/devices/template` folder. - - Create a new folder for your device under `local/devices` directory. - - Copy the `device_config.json` file from `resources/devices/template` to the newly created device folder. - -This ensures that you have a copy of the default configuration file, which you can then modify for your specific device. - -> Note: Ensure that the device configuration file is properly formatted, and the changes made align with the intended test behavior. Incorrect settings or syntax may lead to unexpected results during testing. - -If you encounter any issues or need assistance with the device configuration, refer to the Testrun documentation or ask a question on the Issues page. diff --git a/docs/dev/README.md b/docs/dev/README.md new file mode 100644 index 000000000..076cb827c --- /dev/null +++ b/docs/dev/README.md @@ -0,0 +1,35 @@ +Testrun logo + +# Developer guidelines + +## How to contribute + +As an open source project, we encourage contributions from the community to help Testrun remain an expanding but stable product. To contribute, follow the steps below: + +1. Sign the [Google Contributor License Agreement (CLA)](https://cla.developers.google.com/). + - Whether you're an individual or contributing on behalf of your organization, you must be covered by a Google CLA. + +1. Determine the scope of your contribution. + - Keep it simple. Your contribution is more likely to be accepted if you change fewer files. + - Ensure your pull request addresses one thing, such as a bug fix, dependency issue, or new framework capability. + +1. Reach out to the core maintainers at [testrun-team@googlegroups.com](mailto:testrun-team@googlegroups.com). + - They can provide confirmation that your proposed changes meet our objectives and align with Testrun principles, making them more likely to be accepted. + +1. Fork Testrun and get developing. + - We aim to provide thorough and clear developer documentation to help you contribute successfully. + +## Code quality + +To ensure code quality, use the appropriate style guide when developing code for Testrun: + +- [Python](https://google.github.io/styleguide/pyguide.html) +- [Angular](https://google.github.io/styleguide/angularjs-google-style.html) +- [Shell](https://google.github.io/styleguide/shellguide.html) +- [HTML/CSS](https://google.github.io/styleguide/htmlcssguide.html) +- [JSON](https://google.github.io/styleguide/jsoncstyleguide.xml) +- [Markdown](https://google.github.io/styleguide/docguide/style.html) + +## Automated actions + +The current code base has zero code lint issues. To maintain this, all lint checks are enforced on pull requests to dev and main. You should ensure these lint checks pass before marking your pull requests as Ready for review. diff --git a/docs/dev/mockoon.json b/docs/dev/mockoon.json index a73eb5beb..8e9590e76 100644 --- a/docs/dev/mockoon.json +++ b/docs/dev/mockoon.json @@ -604,8 +604,8 @@ "endpoint": "reports", "responses": [ { - "uuid": "9536ff4c-f97f-4880-b9fc-f477686ad6b8", - "body": "[\n {\n \"mac_addr\": \"00:1e:42:35:73:c6\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"firmware\": \"1.2.3\",\n \"test_modules\": {\n \"connection\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": true\n },\n \"tls\": {\n \"enabled\": true\n },\n \"protocol\": {\n \"enabled\": true\n }\n }\n },\n \"status\": \"Non-Compliant\",\n \"started\": \"2024-05-03 12:09:59\",\n \"finished\": \"2024-05-03 12:15:51\",\n \"tests\": {\n \"total\": 20,\n \"results\": [\n {\n \"name\": \"protocol.valid_bacnet\",\n \"description\": \"BACnet discovery could not resolve any devices\",\n \"expected_behavior\": \"BACnet traffic can be seen on the network and packets are valid and not malformed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Skipped\"\n },\n {\n \"name\": \"protocol.bacnet.version\",\n \"description\": \"No BACnet devices discovered.\",\n \"expected_behavior\": \"The BACnet client implements an up to date version of BACnet\",\n \"required_result\": \"Recommended\",\n \"result\": \"Skipped\"\n },\n {\n \"name\": \"protocol.valid_modbus\",\n \"description\": \"Failed to establish Modbus connection to device\",\n \"expected_behavior\": \"Any Modbus functionality works as expected and valid Modbus traffic can be observed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Non-Compliant\"\n },\n {\n \"name\": \"ntp.network.ntp_support\",\n \"description\": \"Device sent NTPv3 packets. NTPv3 is not allowed.\",\n \"expected_behavior\": \"The device sends an NTPv4 request to the configured NTP server.\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\"\n },\n {\n \"name\": \"ntp.network.ntp_dhcp\",\n \"description\": \"Device sent NTP request to non-DHCP provided server\",\n \"expected_behavior\": \"Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)\",\n \"required_result\": \"Roadmap\",\n \"result\": \"Non-Compliant\"\n },\n {\n \"name\": \"security.services.ftp\",\n \"description\": \"No FTP server found\",\n \"expected_behavior\": \"There is no FTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.ssh.version\",\n \"description\": \"SSH server found running protocol 2.0\",\n \"expected_behavior\": \"SSH server is not running or server is SSHv2\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.telnet\",\n \"description\": \"No telnet server found\",\n \"expected_behavior\": \"There is no Telnet service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.smtp\",\n \"description\": \"No SMTP server found\",\n \"expected_behavior\": \"There is no SMTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.http\",\n \"description\": \"Found HTTP server running on port 80/tcp\",\n \"expected_behavior\": \"Device is unreachable on port 80 (or any other port) and only responds to HTTPS requests on port 443 (or any other port if HTTP is used at all)\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\"\n },\n {\n \"name\": \"security.services.pop\",\n \"description\": \"No POP server found\",\n \"expected_behavior\": \"There is no POP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.imap\",\n \"description\": \"No IMAP server found\",\n \"expected_behavior\": \"There is no IMAP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.snmpv3\",\n \"description\": \"No SNMP server found\",\n \"expected_behavior\": \"Device is unreachable on port 161 (or any other port) and device is unreachable on port 162 (or any other port) unless SNMP is essential in which case it is SNMPv3 is used.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.vnc\",\n \"description\": \"No VNC server found\",\n \"expected_behavior\": \"Device cannot be accessed / connected to via VNC on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.tftp\",\n \"description\": \"No TFTP server found\",\n \"expected_behavior\": \"There is no TFTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"ntp.network.ntp_server\",\n \"description\": \"No NTP server found\",\n \"expected_behavior\": \"The device does not respond to NTP requests when it's IP is set as the NTP server on another device\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"DNS traffic detected from device\",\n \"expected_behavior\": \"The device sends DNS requests.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"DNS traffic detected only to DHCP provided server\",\n \"expected_behavior\": \"The device sends DNS requests to the DNS server provided by the DHCP server\",\n \"required_result\": \"Roadmap\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.tls.v1_2_server\",\n \"description\": \"TLS 1.2 not validated: Certificate has expired\\nEC key length passed: 256 >= 224\\nDevice certificate has not been signed\\nTLS 1.3 not validated: Certificate has expired\\nEC key length passed: 256 >= 224\\nDevice certificate has not been signed\",\n \"expected_behavior\": \"TLS 1.2 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\"\n },\n {\n \"name\": \"security.tls.v1_2_client\",\n \"description\": \"No outbound connections were found.\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers\",\n \"required_result\": \"Required\",\n \"result\": \"Skipped\"\n }\n ]\n },\n \"report\": \"http://localhost:8000/report/123 123/2024-05-03T12:09:59\"\n }\n]", + "uuid": "6adc954a-55c9-40ed-8f49-cf38f659d883", + "body": "[\n {\n \"mac_addr\": \"00:1e:42:35:73:c6\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"firmware\": \"1.2.3\",\n \"test_modules\": {\n \"connection\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": true\n },\n \"tls\": {\n \"enabled\": true\n },\n \"protocol\": {\n \"enabled\": true\n }\n }\n },\n \"status\": \"Non-Compliant\",\n \"started\": \"2024-05-03 12:09:59\",\n \"finished\": \"2024-05-03 12:15:51\",\n \"tests\": {\n \"total\": 20,\n \"results\": [\n {\n \"name\": \"protocol.valid_bacnet\",\n \"description\": \"BACnet discovery could not resolve any devices\",\n \"expected_behavior\": \"BACnet traffic can be seen on the network and packets are valid and not malformed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Skipped\"\n },\n {\n \"name\": \"protocol.bacnet.version\",\n \"description\": \"No BACnet devices discovered.\",\n \"expected_behavior\": \"The BACnet client implements an up to date version of BACnet\",\n \"required_result\": \"Recommended\",\n \"result\": \"Skipped\"\n },\n {\n \"name\": \"protocol.valid_modbus\",\n \"description\": \"Failed to establish Modbus connection to device\",\n \"expected_behavior\": \"Any Modbus functionality works as expected and valid Modbus traffic can be observed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Non-Compliant\"\n },\n {\n \"name\": \"ntp.network.ntp_support\",\n \"description\": \"Device sent NTPv3 packets. NTPv3 is not allowed.\",\n \"expected_behavior\": \"The device sends an NTPv4 request to the configured NTP server.\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\"\n },\n {\n \"name\": \"ntp.network.ntp_dhcp\",\n \"description\": \"Device sent NTP request to non-DHCP provided server\",\n \"expected_behavior\": \"Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)\",\n \"required_result\": \"Roadmap\",\n \"result\": \"Non-Compliant\"\n },\n {\n \"name\": \"security.services.ftp\",\n \"description\": \"No FTP server found\",\n \"expected_behavior\": \"There is no FTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.ssh.version\",\n \"description\": \"SSH server found running protocol 2.0\",\n \"expected_behavior\": \"SSH server is not running or server is SSHv2\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.telnet\",\n \"description\": \"No telnet server found\",\n \"expected_behavior\": \"There is no Telnet service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.smtp\",\n \"description\": \"No SMTP server found\",\n \"expected_behavior\": \"There is no SMTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.http\",\n \"description\": \"Found HTTP server running on port 80/tcp\",\n \"expected_behavior\": \"Device is unreachable on port 80 (or any other port) and only responds to HTTPS requests on port 443 (or any other port if HTTP is used at all)\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\"\n },\n {\n \"name\": \"security.services.pop\",\n \"description\": \"No POP server found\",\n \"expected_behavior\": \"There is no POP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.imap\",\n \"description\": \"No IMAP server found\",\n \"expected_behavior\": \"There is no IMAP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.snmpv3\",\n \"description\": \"No SNMP server found\",\n \"expected_behavior\": \"Device is unreachable on port 161 (or any other port) and device is unreachable on port 162 (or any other port) unless SNMP is essential in which case it is SNMPv3 is used.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.vnc\",\n \"description\": \"No VNC server found\",\n \"expected_behavior\": \"Device cannot be accessed / connected to via VNC on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.tftp\",\n \"description\": \"No TFTP server found\",\n \"expected_behavior\": \"There is no TFTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"ntp.network.ntp_server\",\n \"description\": \"No NTP server found\",\n \"expected_behavior\": \"The device does not respond to NTP requests when it's IP is set as the NTP server on another device\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"DNS traffic detected from device\",\n \"expected_behavior\": \"The device sends DNS requests.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"DNS traffic detected only to DHCP provided server\",\n \"expected_behavior\": \"The device sends DNS requests to the DNS server provided by the DHCP server\",\n \"required_result\": \"Roadmap\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.tls.v1_2_server\",\n \"description\": \"TLS 1.2 not validated: Certificate has expired\\nEC key length passed: 256 >= 224\\nDevice certificate has not been signed\\nTLS 1.3 not validated: Certificate has expired\\nEC key length passed: 256 >= 224\\nDevice certificate has not been signed\",\n \"expected_behavior\": \"TLS 1.2 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\"\n },\n {\n \"name\": \"security.tls.v1_2_client\",\n \"description\": \"No outbound TLS connections were found.\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers\",\n \"required_result\": \"Required\",\n \"result\": \"Skipped\"\n }\n ]\n },\n \"report\": \"http://localhost:8000/report/123 123/2024-05-03T12:09:59\",\n \"export\": \"http://localhost:8000/export/123 123/2024-05-03T12:09:59\"\n \n }\n]", "latency": 0, "statusCode": 200, "label": "", @@ -885,10 +885,10 @@ }, { "uuid": "220e4ba9-6463-4dc3-b714-f77643706b7d", - "body": "{\n \"error\": \"An error occured whilst deleting the report\"\n}", + "body": "{\n \"error\": \"An error occurred whilst deleting the report\"\n}", "latency": 0, "statusCode": 500, - "label": "Error occured", + "label": "Error occurred", "headers": [], "bodyType": "INLINE", "filePath": "", @@ -971,10 +971,10 @@ }, { "uuid": "a7fbb2c8-81dc-4a40-80d6-482119314086", - "body": "{\n \"error\": \"An error occured whilst getting the report\"\n}", + "body": "{\n \"error\": \"An error occurred whilst getting the report\"\n}", "latency": 0, "statusCode": 500, - "label": "Error occured", + "label": "Error occurred", "headers": [], "bodyType": "INLINE", "filePath": "", @@ -1151,10 +1151,10 @@ }, { "uuid": "3bcb2d6d-3290-43bb-8392-7bfffda4feae", - "body": "{\n \"error\": \"An error occured whilst getting the test attempt\"\n}", + "body": "{\n \"error\": \"An error occurred whilst getting the test attempt\"\n}", "latency": 0, "statusCode": 500, - "label": "Error occured", + "label": "Error occurred", "headers": [], "bodyType": "INLINE", "filePath": "", @@ -1602,6 +1602,111 @@ } ], "responseMode": null + }, + { + "uuid": "af7fdcb0-721d-4198-a8ef-c6d8c4eba8c8", + "type": "http", + "documentation": "Get a Testrun PDF profile", + "method": "post", + "endpoint": "report/{profile_name}", + "responses": [ + { + "uuid": "9a759f46-4bc4-433a-be86-e456f069c217", + "body": "", + "latency": 0, + "statusCode": 200, + "label": "Profile found - no device selected", + "headers": [], + "bodyType": "FILE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "c9a09ae7-3158-4956-93ac-4c8a90dfced8", + "body": "", + "latency": 0, + "statusCode": 200, + "label": "Profile found - device selected ", + "headers": [], + "bodyType": "FILE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "5f98471e-15b6-47a4-a68d-e98c3a538b40", + "body": "{\n \"error\": \"Profile could not be found\"\n}", + "latency": 0, + "statusCode": 404, + "label": "Profile not found", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "767d9e78-386e-4bf7-bec8-71a005efdce9", + "body": "{\n \"error\": \"A device with that mac address could not be found\"\n}", + "latency": 0, + "statusCode": 404, + "label": "Device not found", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "5d76bea0-39c1-45f2-80f1-de6f770cb999", + "body": "{\n \"error\": \"Error retrieving the profile PDF\"\n}", + "latency": 0, + "statusCode": 500, + "label": "Error occurred", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null } ], "rootChildren": [ @@ -1700,6 +1805,10 @@ { "type": "route", "uuid": "26f0f76f-e787-4ebe-a3f8-ea3a6004bc15" + }, + { + "type": "route", + "uuid": "af7fdcb0-721d-4198-a8ef-c6d8c4eba8c8" } ], "proxyMode": false, diff --git a/docs/dev/postman.json b/docs/dev/postman.json index 39e4529b3..08369ac55 100644 --- a/docs/dev/postman.json +++ b/docs/dev/postman.json @@ -1,9 +1,10 @@ { "info": { - "_postman_id": "3d270980-478e-4243-9130-ee5cab278ed5", + "_postman_id": "f42dd4c6-e1a3-4ae3-a991-24cceb4d2627", "name": "Testrun", + "description": "API endpoints for the Testrun API", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "28312403" + "_exporter_id": "37950585" }, "item": [ { @@ -21,11 +22,12 @@ "system", "interfaces" ] - } + }, + "description": "Obtain a list of applicable and available network adapters for use in testing" }, "response": [ { - "name": "Interfaces", + "name": "Interfaces Available (200)", "originalRequest": { "method": "GET", "header": [], @@ -43,10 +45,100 @@ }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], "body": "{\n \"enx00e04c020fa8\": \"00:e0:4c:02:0f:a8\",\n \"enx207bd26205e9\": \"20:7b:d2:62:05:e9\"\n}" + }, + { + "name": "No interfaces (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/interfaces", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "interfaces" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{}" + } + ] + }, + { + "name": "Get Configuration", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/config", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config" + ] + }, + "description": "Get the current system configuration" + }, + "response": [ + { + "name": "Get Configuration (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/config", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"INFO\",\n \"startup_timeout\": 60,\n \"monitor_period\": 60,\n \"runtime\": 120\n}" } ] }, @@ -57,7 +149,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"startup_timeout\": 60,\n \"monitor_period\": 20,\n \"runtime\": 120\n}", + "raw": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"startup_timeout\": 60,\n \"monitor_period\": 20,\n \"runtime\": 120,\n \"org_name\": \"Google\"\n}", "options": { "raw": { "language": "json" @@ -74,17 +166,18 @@ "system", "config" ] - } + }, + "description": "Update the current system configuration" }, "response": [ { - "name": "Configuration Set", + "name": "Configuration Set (200)", "originalRequest": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120,\n \"startup_timeout\": 60,\n \"monitor_period\": 60 \n}", + "raw": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120, // Optional\n \"startup_timeout\": 60, // Optional\n \"monitor_period\": 60 // Optional\n}", "options": { "raw": { "language": "json" @@ -105,13 +198,20 @@ }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120,\n \"startup_timeout\": 60,\n \"monitor_period\": 60 \n}" + "body": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120,\n \"startup_timeout\": 60,\n \"monitor_period\": 60\n}" }, { - "name": "Invalid JSON", + "name": "Invalid JSON (400)", "originalRequest": { "method": "POST", "header": [], @@ -138,8 +238,15 @@ }, "status": "Bad Request", "code": 400, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], "body": "{\n \"error\": \"Invalid JSON received\"\n}" } @@ -152,7 +259,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"firmware\": \"1.2.2\"\n }\n}", + "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"tls\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n }\n}", "options": { "raw": { "language": "json" @@ -169,11 +276,52 @@ "system", "start" ] - } + }, + "description": "Start Testrun against a target device" }, "response": [ { - "name": "Invalid request", + "name": "Starting (200)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"tls\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/system/start", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "start" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"status\": \"Waiting for Device\",\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-07-18T11:14:42.917670\",\n \"finished\": null, \n \"report\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}" + }, + { + "name": "Invalid Request (400)", "originalRequest": { "method": "POST", "header": [], @@ -200,19 +348,26 @@ }, "status": "Bad Request", "code": 400, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], "body": "{\n \"error\": \"Invalid request received\"\n}" }, { - "name": "Starting", + "name": "Invalid JSON (400)", "originalRequest": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"firmware\": \"1.2.2\"\n }\n}", + "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff,\n \"firmware\": \"1.2.2\"\n }\n}", "options": { "raw": { "language": "json" @@ -231,15 +386,22 @@ ] } }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": null, - "header": null, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"status\": \"Starting\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB 140\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": false\n }\n }\n }\n \"started\": \"2023-07-18T11:14:42.917670\",\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}" + "body": "{\n \"error\": \"Invalid JSON received\"\n}" }, { - "name": "Device Not Found", + "name": "Device Not Found (404)", "originalRequest": { "method": "POST", "header": [], @@ -266,19 +428,26 @@ }, "status": "Not Found", "code": 404, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"error\": \"Device not found\"\n}" + "body": "{\n \"error\": \"A device with that MAC address could not be found\"\n}" }, { - "name": "Invalid JSON", + "name": "Testrun Already in Progress (409)", "originalRequest": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff,\n \"firmware\": \"1.2.2\"\n }\n}", + "raw": "{\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n }\n }\n }\n}", "options": { "raw": { "language": "json" @@ -297,12 +466,19 @@ ] } }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": null, - "header": null, + "status": "Conflict", + "code": 409, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"error\": \"Invalid JSON received\"\n}" + "body": "{\n \"error\": \"Testrun cannot be started whilst a test is running on another device\"\n}" } ] }, @@ -330,11 +506,12 @@ "system", "stop" ] - } + }, + "description": "Stop Testrun from running against a device" }, "response": [ { - "name": "Not Running", + "name": "Stopped (200)", "originalRequest": { "method": "POST", "header": [], @@ -359,15 +536,22 @@ ] } }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": null, - "header": null, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"error\": \"Test Run is not running\"\n}" + "body": "{\n \"success\": \"Testrun stopped\"\n}" }, { - "name": "Stopped", + "name": "Not Running (404)", "originalRequest": { "method": "POST", "header": [], @@ -392,168 +576,151 @@ ] } }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": null, - "header": null, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"success\": \"Test Run has stopped\"\n}" + "body": "{\n \"error\": \"Testrun is not currently running\"\n}" } ] }, { - "name": "Get Devices", + "name": "Get System Status", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, "request": { "method": "GET", "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "localhost:8000/devices", + "raw": "localhost:8000/system/status", "host": [ "localhost" ], "port": "8000", "path": [ - "devices" + "system", + "status" ] - } + }, + "description": "Get the current status of Testrun. Suggested that this is called every 5 seconds to capture most events" }, "response": [ { - "name": "Devices", + "name": "Monitoring (200)", "originalRequest": { "method": "GET", "header": [], "url": { - "raw": "localhost:8000/devices", + "raw": "localhost:8000/system/status", "host": [ "localhost" ], "port": "8000", "path": [ - "devices" + "system", + "status" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "[\n {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB 140\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": false\n }\n }\n },\n {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"manufacturer\": \"Manufacturer X\",\n \"model\": \"Device X\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n }\n }\n }\n]" + "body": "{\n \"status\": \"Monitoring\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"report\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}" }, { - "name": "No Devices", + "name": "Test in Progress (200)", "originalRequest": { "method": "GET", "header": [], "url": { - "raw": "localhost:8000/devices", + "raw": "localhost:8000/system/status", "host": [ "localhost" ], "port": "8000", "path": [ - "devices" + "system", + "status" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, - "cookie": [], - "body": "[\n\n]" - } - ] - }, - { - "name": "Get Configuration", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "localhost:8000/system/config", - "host": [ - "localhost" + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } ], - "port": "8000", - "path": [ - "system", - "config" - ] - } - }, - "response": [ + "cookie": [], + "body": "{\n \"status\": \"In Progress\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Non-Compliant\"\n }\n ]\n }\n}" + }, { - "name": "Configuration", + "name": "Cancelled (200)", "originalRequest": { "method": "GET", "header": [], "url": { - "raw": "localhost:8000/system/config", + "raw": "localhost:8000/system/status", "host": [ "localhost" ], "port": "8000", "path": [ "system", - "config" + "status" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, - "cookie": [], - "body": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"INFO\",\n \"startup_timeout\": 60,\n \"monitor_period\": 60,\n \"runtime\": 120\n}" - } - ] - }, - { - "name": "Get System Status", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" } - } - }, - "url": { - "raw": "localhost:8000/system/status", - "host": [ - "localhost" ], - "port": "8000", - "path": [ - "system", - "status" - ] - } - }, - "response": [ + "cookie": [], + "body": "{\n \"status\": \"Cancelled\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": \"2023-06-22T09:24:00.123Z\",\n \"report\": null,\n \"tests\": {\n \"total\": 22,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Feature Not Present\",\n \"required_result\": \"Roadmap\"\n }\n ]\n }\n}" + }, { - "name": "Test In Progress", + "name": "Waiting for Device (200)", "originalRequest": { "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "localhost:8000/system/status", "host": [ @@ -568,25 +735,23 @@ }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"status\": \"In Progress\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Non-Compliant\"\n } \n ]\n }\n}" + "body": "{\n \"status\": \"Waiting for Device\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"report\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}" }, { - "name": "Not Running", + "name": "Non-Compliant (200)", "originalRequest": { "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "localhost:8000/system/status", "host": [ @@ -601,83 +766,148 @@ }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"status\": \"Non-Compliant\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": \"2023-06-22T09:26:00.123Z\",\n \"report\": \"https://api.testrun.io/report.pdf\",\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Non-Compliant\",\n \"required_result\": \"Compliant\"\n \"recommendations\": [\n \"An example of a step to resolve\",\n \"Disable any running NTP server\"\n ]\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Compliant\",\n \"required_result\": \"Roadmap\"\n },\n {\n \"name\": \"security.services.ftp\",\n \"description\": \"FTP server should not be available\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n }\n ]\n }\n}" + }, + { + "name": "Compliant (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/status", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "status" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"status\": \"Compliant\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": \"2023-06-22T09:26:00.123Z\",\n \"report\": \"https://api.testrun.io/report.pdf\",\n \"tests\": {\n \"total\": 3,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Feature Not Present\",\n \"required_result\": \"Roadmap\"\n },\n {\n \"name\": \"security.services.ftp\",\n \"description\": \"FTP server should not be available\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n }\n ]\n }\n}" + }, + { + "name": "Not Running - Idle (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/status", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "status" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"status\": \"Idle\",\n \"device\": null,\n \"started\": null,\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": [\n ]\n }\n}" + "body": "{\n \"status\": \"Idle\",\n \"device\": null,\n \"started\": null,\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}" } ] }, { - "name": "Get Test Reports", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, + "name": "Shutdown Testrun", "request": { - "method": "GET", + "method": "POST", "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "localhost:8000/reports", + "raw": "http://localhost:8000/system/shutdown", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "reports" + "system", + "shutdown" ] - } + }, + "description": "Stop the Testrun framework" }, "response": [ { - "name": "Test In Progress", + "name": "Shutdown Testrun (200)", "originalRequest": { - "method": "GET", + "method": "POST", "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "localhost:8000/system/status", + "raw": "http://localhost:8000/system/shutdown", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ "system", - "status" + "shutdown" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"status\": \"In Progress\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Non-Compliant\"\n } \n ]\n }\n}" + "body": "null" }, { - "name": "Not Running", + "name": "Test in Progress (400)", "originalRequest": { - "method": "GET", - "header": [], + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], "body": { "mode": "raw", - "raw": "", + "raw": "{\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n }\n }\n }\n}", "options": { "raw": { "language": "json" @@ -685,34 +915,99 @@ } }, "url": { - "raw": "localhost:8000/system/status", + "raw": "http://localhost:8000/system/shutdown", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ "system", - "status" + "shutdown" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Unable to shutdown. A test is currently in progress.\"\n}" + } + ] + }, + { + "name": "Get Version", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/version", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "version" + ] + }, + "description": "Get the current Testrun version and check if a software update is available" + }, + "response": [ + { + "name": "Get Version (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/version", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "version" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"status\": \"Idle\",\n \"device\": null,\n \"started\": null,\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": [\n ]\n }\n}" + "body": "{\n \"installed_version\": \"v2.0\",\n \"update_available\": false,\n \"latest_version\": \"v2.0\",\n \"latest_version_url\": \"https://github.com/google/testrun/releases/tag/v2.0\"\n}" } ] }, { - "name": "Create Device", + "name": "Get Test Reports", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, "request": { - "method": "POST", + "method": "GET", "header": [], "body": { "mode": "raw", - "raw": "{\n \"manufacturer\": \"Delta\",\n \"mac_addr\": \"00:1e:42:35:73:c1\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"connection\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n },\n \"dns\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": false\n }\n }\n}", + "raw": "", "options": { "raw": { "language": "json" @@ -720,91 +1015,107 @@ } }, "url": { - "raw": "localhost:8000/device", + "raw": "localhost:8000/reports", "host": [ "localhost" ], "port": "8000", "path": [ - "device" + "reports" ] - } + }, + "description": "Get all previous Testrun reports" }, "response": [ { - "name": "Create Device", + "name": "Get Test Reports (200)", "originalRequest": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } + "method": "GET", + "header": [ + { + "key": "Origin", + "value": "http://localhost:8000", + "type": "text" } - }, + ], "url": { - "raw": "localhost:8000/device", + "raw": "localhost:8000/reports", "host": [ "localhost" ], "port": "8000", "path": [ - "device" + "reports" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": [\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n ]\n}" + "body": "[\n {\n \"testrun\": {\n \"version\": \"2.0\"\n },\n \"mac_addr\": \"00:1e:42:28:9e:4a\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:28:9e:4a\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"firmware\": \"test\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"tls\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n },\n \"status\": \"Non-Compliant\",\n \"started\": \"2000-01-01 00:00:00\",\n \"finished\": \"2000-01-01 00:30:00\",\n \"tests\": {\n \"total\": 40,\n \"results\": [\n {\n \"name\": \"protocol.valid_bacnet\",\n \"description\": \"BACnet device could not be discovered\",\n \"expected_behavior\": \"BACnet traffic can be seen on the network and packets are valid and not malformed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"protocol.bacnet.version\",\n \"description\": \"Device did not respond to BACnet discovery\",\n \"expected_behavior\": \"The BACnet client implements an up to date version of BACnet\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"protocol.valid_modbus\",\n \"description\": \"Device did not respond to Modbus connection\",\n \"expected_behavior\": \"Any Modbus functionality works as expected and valid Modbus traffic can be observed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"security.services.ftp\",\n \"description\": \"No FTP server found\",\n \"expected_behavior\": \"There is no FTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.ssh.version\",\n \"description\": \"SSH server found running protocol 2.0\",\n \"expected_behavior\": \"SSH server is not running or server is SSHv2\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.telnet\",\n \"description\": \"No telnet server found\",\n \"expected_behavior\": \"There is no Telnet service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.smtp\",\n \"description\": \"No SMTP server found\",\n \"expected_behavior\": \"There is no SMTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.http\",\n \"description\": \"Found HTTP server running on port 80/tcp\",\n \"expected_behavior\": \"Device is unreachable on port 80 (or any other port) and only responds to HTTPS requests on port 443 (or any other port if HTTP is used at all)\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Disable all unsecure HTTP servers\",\n \"Setup TLS on the web server\"\n ]\n },\n {\n \"name\": \"security.services.pop\",\n \"description\": \"No POP server found\",\n \"expected_behavior\": \"There is no POP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.imap\",\n \"description\": \"No IMAP server found\",\n \"expected_behavior\": \"There is no IMAP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.snmpv3\",\n \"description\": \"No SNMP server found\",\n \"expected_behavior\": \"Device is unreachable on port 161 (or any other port) and device is unreachable on port 162 (or any other port) unless SNMP is essential in which case it is SNMPv3 is used.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.vnc\",\n \"description\": \"No VNC server found\",\n \"expected_behavior\": \"Device cannot be accessed / connected to via VNC on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.tftp\",\n \"description\": \"No TFTP server found\",\n \"expected_behavior\": \"There is no TFTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"ntp.network.ntp_server\",\n \"description\": \"No NTP server found\",\n \"expected_behavior\": \"The device does not respond to NTP requests when it's IP is set as the NTP server on another device\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.port_link\",\n \"description\": \"No port errors detected\",\n \"expected_behavior\": \"When the etherent cable is connected to the port, the device triggers the port to its enabled \\\"Link UP\\\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \\\"show interface\\\" command on most network switches.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.port_speed\",\n \"description\": \"Succesfully auto-negotiated speeds above 10 Mbps\",\n \"expected_behavior\": \"When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \\\"show interface\\\" command on most network switches. The output of this command must also show that the \\\"configured speed\\\" is set to \\\"auto\\\".\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.port_duplex\",\n \"description\": \"Succesfully auto-negotiated full duplex\",\n \"expected_behavior\": \"When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.switch.arp_inspection\",\n \"description\": \"Device uses ARP\",\n \"expected_behavior\": \"Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.switch.dhcp_snooping\",\n \"description\": \"Device does not act as a DHCP server\",\n \"expected_behavior\": \"Device continues to operate correctly when DHCP snooping is enabled on the switch.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.dhcp_address\",\n \"description\": \"Device responded to leased ip address\",\n \"expected_behavior\": \"The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.mac_address\",\n \"description\": \"MAC address found: 00:1e:42:28:9e:4a\",\n \"expected_behavior\": \"N/A\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.mac_oui\",\n \"description\": \"OUI Manufacturer found: Teltonika\",\n \"expected_behavior\": \"The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.private_address\",\n \"description\": \"All subnets are supported\",\n \"expected_behavior\": \"The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.shared_address\",\n \"description\": \"All subnets are supported\",\n \"expected_behavior\": \"The device under test accepts IP addresses within the ranges specified in RFC 6598 and communicates using these addresses\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.dhcp_disconnect\",\n \"description\": \"An error occurred whilst running this test\",\n \"expected_behavior\": \"A client SHOULD use DHCP to reacquire or verify its IP address and network parameters whenever the local network parameters may have changed; e.g., at system boot time or after a disconnection from the local network, as the local network configuration may change without the client's or user's knowledge. If a client has knowledge ofa previous network address and is unable to contact a local DHCP server, the client may continue to use the previous network addres until the lease for that address expires. If the lease expires before the client can contact a DHCP server, the client must immediately discontinue use of the previous network address and may inform local users of the problem.\",\n \"required_result\": \"Required\",\n \"result\": \"Error\"\n },\n {\n \"name\": \"connection.single_ip\",\n \"description\": \"Device is using multiple IP addresses\",\n \"expected_behavior\": \"The device under test does not behave as a network switch and only requets one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Ensure that all ports on the device are isolated\",\n \"Ensure only one DHCP client is running\"\n ]\n },\n {\n \"name\": \"connection.target_ping\",\n \"description\": \"Device responds to ping\",\n \"expected_behavior\": \"The device under test responds to an ICMP echo (ping) request.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.ipaddr.ip_change\",\n \"description\": \"Device has accepted an IP address change\",\n \"expected_behavior\": \"If the lease expires before the client receiveds a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network addres, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.ipaddr.dhcp_failover\",\n \"description\": \"Secondary DHCP server lease confirmed active in device\",\n \"expected_behavior\": \"\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.ipv6_slaac\",\n \"description\": \"Device does not support IPv6 SLAAC\",\n \"expected_behavior\": \"The device under test complies with RFC4862 and forms a valid IPv6 SLAAC address\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Install a network manager that supports IPv6\",\n \"Disable DHCPv6\"\n ]\n },\n {\n \"name\": \"connection.ipv6_ping\",\n \"description\": \"No IPv6 SLAAC address found. Cannot ping\",\n \"expected_behavior\": \"The device responds to the ping as per RFC4443\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Enable ping response to IPv6 ICMP requests in network manager settings\",\n \"Create a firewall exception to allow ICMPv6 via LAN\"\n ]\n },\n {\n \"name\": \"security.tls.v1_2_server\",\n \"description\": \"TLS 1.2 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.2 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Enable TLS 1.2 support in the web server configuration\",\n \"Disable TLS 1.0 and 1.1\",\n \"Sign the certificate used by the web server\"\n ]\n },\n {\n \"name\": \"security.tls.v1_2_client\",\n \"description\": \"No outbound TLS connections were found\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"security.tls.v1_3_server\",\n \"description\": \"TLS 1.3 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.3 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n },\n {\n \"name\": \"security.tls.v1_3_client\",\n \"description\": \"No outbound TLS connections were found\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.3\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n },\n {\n \"name\": \"ntp.network.ntp_support\",\n \"description\": \"Device has not sent any NTP requests\",\n \"expected_behavior\": \"The device sends an NTPv4 request to the configured NTP server.\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Set the NTP version to v4 in the NTP client\",\n \"Install an NTP client that supports NTPv4\"\n ]\n },\n {\n \"name\": \"ntp.network.ntp_dhcp\",\n \"description\": \"Device has not sent any NTP requests\",\n \"expected_behavior\": \"Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)\",\n \"required_result\": \"Roadmap\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"DNS traffic detected from device\",\n \"expected_behavior\": \"The device sends DNS requests.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"DNS traffic detected only to DHCP provided server\",\n \"expected_behavior\": \"The device sends DNS requests to the DNS server provided by the DHCP server\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n },\n {\n \"name\": \"dns.mdns\",\n \"description\": \"No MDNS traffic detected from the device\",\n \"expected_behavior\": \"Device may send MDNS requests\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n }\n ]\n },\n \"report\": \"http://localhost:8000/report/Teltonika TRB140/2024-09-10T13:19:24\",\n \"export\": \"http://localhost:8000/export/Teltonika TRB140/2024-09-10T13:19:24\"\n }\n]" }, { - "name": "Invalid JSON", + "name": "No Test Reports (200)", "originalRequest": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } + "method": "GET", + "header": [ + { + "key": "Origin", + "value": "http://localhost:8000", + "type": "text" } - }, + ], "url": { - "raw": "localhost:8000/device", + "raw": "localhost:8000/reports", "host": [ "localhost" ], "port": "8000", "path": [ - "device" + "reports" ] } }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": null, - "header": null, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"error\": \"Invalid JSON received\"\n}" + "body": "[]" } ] }, { - "name": "Edit Device", + "name": "Delete Report", "request": { - "method": "POST", + "method": "DELETE", "header": [], "body": { "mode": "raw", - "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c1\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"test_modules\": {\n \"connection\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n },\n \"dns\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": false\n }\n }\n }\n}", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2023-10-10 16:53:47\"\n}", "options": { "raw": { "language": "json" @@ -812,26 +1123,27 @@ } }, "url": { - "raw": "localhost:8000/device/edit", + "raw": "http://localhost:8000/report", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "device", - "edit" + "report" ] - } + }, + "description": "Delete a previous Testrun report" }, "response": [ { - "name": "Create Device", + "name": "Report Deleted (200)", "originalRequest": { - "method": "POST", + "method": "DELETE", "header": [], "body": { "mode": "raw", - "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2024-08-05 13:37:53\"\n}", "options": { "raw": { "language": "json" @@ -839,31 +1151,39 @@ } }, "url": { - "raw": "localhost:8000/device", + "raw": "http://localhost:8000/report", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "device" + "report" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": [\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n ]\n}" + "body": "{\n \"success\": \"Deleted report\"\n}" }, { - "name": "Invalid JSON", + "name": "Invalid JSON (400)", "originalRequest": { - "method": "POST", + "method": "DELETE", "header": [], "body": { "mode": "raw", - "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}", + "raw": "{\n \n}", "options": { "raw": { "language": "json" @@ -871,171 +1191,2871 @@ } }, "url": { - "raw": "localhost:8000/device", + "raw": "http://localhost:8000/report", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "device" + "report" ] } }, "status": "Bad Request", "code": 400, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"error\": \"Invalid JSON received\"\n}" + "body": "{\n \"error\": \"Invalid request received\"\n}" + }, + { + "name": "Report Not Found (404))", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2023-10-10 16:53:47\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/report", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "report" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Report not found\"\n}" } ] }, { - "name": "Delete Report", + "name": "Get Report", "request": { - "method": "DELETE", + "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2023-10-10 16:53:47\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "http://localhost:8000/report", + "raw": "http://localhost:8000/report/{device_name}/{timestamp}", "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "report" + "report", + "{device_name}", + "{timestamp}" ] - } + }, + "description": "Get the PDF report for a specific device and timestamp" }, - "response": [] - }, - { - "name": "Get Version", + "response": [ + { + "name": "Get Report (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/report/Teltonika TRB140/2000-01-01T00:00:00", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "report", + "Teltonika TRB140", + "2000-01-01T00:00:00" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "application/pdf", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "report.pdf" + }, + { + "name": "Report Not Found (404)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/report/Teltonika TRB140/2024-05-05 13:37:53", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "report", + "Teltonika TRB140", + "2024-05-05 13:37:53" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Report could not be found\"\n}" + } + ] + }, + { + "name": "Export Report", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"profile\": \"Primary profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/export/{device_name}/{timestamp}", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "export", + "{device_name}", + "{timestamp}" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + }, + "description": "Export the ZIP file of a specific device testing report" + }, + "response": [ + { + "name": "Export without profile (200)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/export/Teltonika TRB140/2024-08-05T13:37:53", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "export", + "Teltonika TRB140", + "2024-08-05T13:37:53" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "application/zip", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "report.zip" + }, + { + "name": "Export with profile (200)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/export/Teltonika TRB140/2024-08-05T13:37:53", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "export", + "Teltonika TRB140", + "2024-08-05T13:37:53" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "Text", + "header": [ + { + "key": "Content-Type", + "value": "application/zip", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "report.zip" + }, + { + "name": "Profile Not Found (404)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"profile\": \"Non-existing profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/export/Teltonika TRB140/2024-06-13T11:31:28", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "export", + "Teltonika TRB140", + "2024-06-13T11:31:28" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A profile with that name could not be found\"\n}" + }, + { + "name": "Report Not Found (404)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/export/Teltonika TRB140/2020-08-05T13:37:53", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "export", + "Teltonika TRB140", + "2020-08-05T13:37:53" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Report not found\"\n}" + } + ] + }, + { + "name": "Get Devices", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/devices", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "devices" + ] + }, + "description": "Obtain the list of devices from the device repository" + }, + "response": [ + { + "name": "Get Devices (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/devices", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "devices" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[\n {\n \"folder_url\": \"local/devices/Teltonika TRB140\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n },\n \"ip_addr\": null,\n \"firmware\": null,\n \"device_folder\": \"Teltonika TRB140\",\n \"reports\": [\n {\n \"_device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"firmware\": \"1.2.3\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n },\n \"_mac_addr\": \"00:1e:42:35:73:c4\",\n \"_status\": \"Non-Compliant\",\n \"_started\": \"2024-08-05T13:37:53\",\n \"_finished\": \"2024-08-05T13:39:35\",\n \"_total_tests\": 12,\n \"_results\": [\n {\n \"name\": \"protocol.valid_bacnet\",\n \"description\": \"BACnet device could not be discovered\",\n \"expected_behavior\": \"BACnet traffic can be seen on the network and packets are valid and not malformed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"protocol.bacnet.version\",\n \"description\": \"Device did not respond to BACnet discovery\",\n \"expected_behavior\": \"The BACnet client implements an up to date version of BACnet\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"protocol.valid_modbus\",\n \"description\": \"Device did not respond to Modbus connection\",\n \"expected_behavior\": \"Any Modbus functionality works as expected and valid Modbus traffic can be observed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"security.tls.v1_2_server\",\n \"description\": \"TLS 1.2 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.2 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Enable TLS 1.2 support in the web server configuration\",\n \"Disable TLS 1.0 and 1.1\",\n \"Sign the certificate used by the web server\"\n ]\n },\n {\n \"name\": \"security.tls.v1_2_client\",\n \"description\": \"TLS 1.2 client connections valid\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Compliant\",\n \"recommendations\": []\n },\n {\n \"name\": \"security.tls.v1_3_server\",\n \"description\": \"TLS 1.3 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.3 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n },\n {\n \"name\": \"security.tls.v1_3_client\",\n \"description\": \"TLS 1.3 client connections valid\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.3\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n },\n {\n \"name\": \"ntp.network.ntp_support\",\n \"description\": \"Device sent NTPv3 packets. NTPv3 is not allowed\",\n \"expected_behavior\": \"The device sends an NTPv4 request to the configured NTP server.\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Set the NTP version to v4 in the NTP client\",\n \"Install an NTP client that supports NTPv4\"\n ]\n },\n {\n \"name\": \"ntp.network.ntp_dhcp\",\n \"description\": \"Device sent NTP request to non-DHCP provided server\",\n \"expected_behavior\": \"Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)\",\n \"required_result\": \"Roadmap\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"DNS traffic detected from device\",\n \"expected_behavior\": \"The device sends DNS requests.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\",\n \"recommendations\": []\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"DNS traffic detected only to DHCP provided server\",\n \"expected_behavior\": \"The device sends DNS requests to the DNS server provided by the DHCP server\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n },\n {\n \"name\": \"dns.mdns\",\n \"description\": \"No MDNS traffic detected from the device\",\n \"expected_behavior\": \"Device may send MDNS requests\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n }\n ],\n \"_module_reports\": [],\n \"_report_url\": \"http://localhost:8000/report/Teltonika TRB140/2024-08-05T13:37:53\",\n \"_cur_page\": 0,\n \"_version\": \"1.3.1\"\n }\n ],\n \"max_device_reports\": null\n },\n {\n \"folder_url\": \"local/devices/Google First\",\n \"mac_addr\": \"00:0e:12:15:13:c8\",\n \"manufacturer\": \"Google\",\n \"model\": \"First\",\n \"test_modules\": null,\n \"ip_addr\": null,\n \"firmware\": null,\n \"device_folder\": \"Google First\",\n \"reports\": [],\n \"max_device_reports\": null\n },\n {\n \"folder_url\": \"local/devices/Delta O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n }\n },\n \"ip_addr\": null,\n \"firmware\": null,\n \"device_folder\": \"Delta O3-DIN-CPU\",\n \"reports\": [],\n \"max_device_reports\": null\n }\n]" + }, + { + "name": "No Devices (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/devices", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "devices" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[]" + } + ] + }, + { + "name": "Create Device", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:55\",\n \"manufacturer\": \"TEST\",\n \"model\": \"Stest2\",\n \"type\": \"IoT Gateway\",\n \"technology\": \"Hardware - Access Control\",\n \"test_pack\": \"Device Qualification\",\n \"additional_info\": [\n {\n \"question\": \"What type of device is this?\",\n \"answer\": \"IoT Gateway\"\n },\n {\n \"question\": \"Please select the technology this device falls into\",\n \"answer\": \"Hardware - Access Control\"\n },\n {\n \"question\": \"Does your device process any sensitive information? \",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Can all non-essential services be disabled on your device?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is there a second IP port on the device?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Can the second IP port on your device be disabled?\",\n \"answer\": \"Yes\"\n }\n ],\n \"test_modules\": {\n \"protocol\": { \"enabled\": true },\n \"services\": { \"enabled\": true },\n \"ntp\": { \"enabled\": true },\n \"tls\": { \"enabled\": true },\n \"connection\": { \"enabled\": true },\n \"dns\": { \"enabled\": true }\n }}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + }, + "description": "Create a new device to test" + }, + "response": [ + { + "name": "Create Device (201)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": null\n}" + }, + { + "name": "Already Exists (409)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Conflict", + "code": 409, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A device with that MAC address already exists\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\"\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid JSON received\"\n}" + }, + { + "name": "Invalid Request (400)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid JSON received\"\n}" + } + ] + }, + { + "name": "Delete device", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + }, + "description": "Create a new device to test" + }, + "response": [ + { + "name": "Delete device (200)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully deleted the device\"\n}" + }, + { + "name": "Not Found (404)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"non-existing\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Device not found\"\n}" + }, + { + "name": "No Mac (400)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid request received\"\n}" + }, + { + "name": "Testrun in Progress (403)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Forbidden", + "code": 403, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Cannot delete this device whilst it is being tested\"\n}" + } + ] + }, + { + "name": "Edit Device", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c1\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"test_modules\": {\n \"connection\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n },\n \"dns\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": false\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + }, + "description": "Update the configuration for a device" + }, + "response": [ + { + "name": "Edit Device (200)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n }\n }\n}" + }, + { + "name": "Not Found (404)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"non-existing\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A device with that MAC address could not be found\"\n}" + }, + { + "name": "Mac Address in Use (409)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:0e:12:15:13:c8\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + } + }, + "status": "Conflict", + "code": 409, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A device with that MAC address already exists\"\n}" + }, + { + "name": "Device is Being Tested (403)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:0e:12:15:13:c8\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + } + }, + "status": "Forbidden", + "code": 403, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Cannot edit this device whilst it is being tested\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n \"model\": \"O3-DIN-CPU\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid request received\"\n}" + } + ] + }, + { + "name": "Get Devices Format", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/devices/format", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "devices", + "format" + ] + }, + "description": "Obtain the list of devices from the device repository" + }, + "response": [ + { + "name": "Get Devices Format (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/devices/format", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "devices", + "format" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[\n {\n \"step\": 1,\n \"title\": \"Select device type & technology\",\n \"description\": \"Before your device can go through testing, tell us more about your device and its functionality. It is important that we fully understand your device before a thorough assessment can be made.\",\n \"questions\": [\n {\n \"id\": 1,\n \"question\": \"What type of device is this?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Building Automation Gateway\",\n \"risk\": \"High\",\n \"id\": 1\n },\n {\n \"text\": \"IoT Gateway\",\n \"risk\": \"High\",\n \"id\": 2\n },\n {\n \"text\": \"Controller - AHU\",\n \"risk\": \"High\",\n \"id\": 3\n },\n {\n \"text\": \"Controller - Boiler\",\n \"risk\": \"High\",\n \"id\": 4\n },\n {\n \"text\": \"Controller - Chiller\",\n \"risk\": \"High\",\n \"id\": 5\n },\n {\n \"text\": \"Controller - FCU\",\n \"risk\": \"Limited\",\n \"id\": 6\n },\n {\n \"text\": \"Controller - Pump\",\n \"risk\": \"Limited\",\n \"id\": 7\n },\n {\n \"text\": \"Controller - CRAC\",\n \"risk\": \"High\",\n \"id\": 8\n },\n {\n \"text\": \"Controller - VAV\",\n \"risk\": \"Limited\",\n \"id\": 9\n },\n {\n \"text\": \"Controller - VRF\",\n \"risk\": \"Limited\",\n \"id\": 10\n },\n {\n \"text\": \"Controller - Multiple\",\n \"risk\": \"High\",\n \"id\": 11\n },\n {\n \"text\": \"Controller - Other\",\n \"risk\": \"High\",\n \"id\": 12\n },\n {\n \"text\": \"Controller - Lighting\",\n \"risk\": \"Limited\",\n \"id\": 13\n },\n {\n \"text\": \"Controller - Blinds/Facades\",\n \"risk\": \"High\",\n \"id\": 14\n },\n {\n \"text\": \"Controller - Lifts/Elevators\",\n \"risk\": \"High\",\n \"id\": 15\n },\n {\n \"text\": \"Controller - UPS\",\n \"risk\": \"High\",\n \"id\": 16\n },\n {\n \"text\": \"Sensor - Air Quality\",\n \"risk\": \"Limited\",\n \"id\": 17\n },\n {\n \"text\": \"Sensor - Vibration\",\n \"risk\": \"Limited\",\n \"id\": 18\n },\n {\n \"text\": \"Sensor - Humidity\",\n \"risk\": \"Limited\",\n \"id\": 19\n },\n {\n \"text\": \"Sensor - Water\",\n \"risk\": \"Limited\",\n \"id\": 20\n },\n {\n \"text\": \"Sensor - Occupancy\",\n \"risk\": \"High\",\n \"id\": 21\n },\n {\n \"text\": \"Sensor - Volume\",\n \"risk\": \"Limited\",\n \"id\": 22\n },\n {\n \"text\": \"Sensor - Weight\",\n \"risk\": \"Limited\",\n \"id\": 23\n },\n {\n \"text\": \"Sensor - Weather\",\n \"risk\": \"Limited\",\n \"id\": 24\n },\n {\n \"text\": \"Sensor - Steam\",\n \"risk\": \"High\",\n \"id\": 25\n },\n {\n \"text\": \"Sensor - Air Flow\",\n \"risk\": \"Limited\",\n \"id\": 26\n },\n {\n \"text\": \"Sensor - Lighting\",\n \"risk\": \"Limited\",\n \"id\": 27\n },\n {\n \"text\": \"Sensor - Other\",\n \"risk\": \"High\",\n \"id\": 28\n },\n {\n \"text\": \"Sensor - Air Quality\",\n \"risk\": \"Limited\",\n \"id\": 29\n },\n {\n \"text\": \"Monitoring - Fire System\",\n \"risk\": \"Limited\",\n \"id\": 30\n },\n {\n \"text\": \"Monitoring - Emergency Lighting\",\n \"risk\": \"Limited\",\n \"id\": 31\n },\n {\n \"text\": \"Monitoring - Other\",\n \"risk\": \"High\",\n \"id\": 32\n },\n {\n \"text\": \"Monitoring - UPS\",\n \"risk\": \"Limited\",\n \"id\": 33\n },\n {\n \"text\": \"Meter - Water\",\n \"risk\": \"Limited\",\n \"id\": 34\n },\n {\n \"text\": \"Meter - Gas\",\n \"risk\": \"Limited\",\n \"id\": 35\n },\n {\n \"text\": \"Meter - Electricity\",\n \"risk\": \"Limited\",\n \"id\": 36\n },\n {\n \"text\": \"Meter - Other\",\n \"risk\": \"High\",\n \"id\": 37\n },\n {\n \"text\": \"Other\",\n \"risk\": \"High\",\n \"id\": 38\n },\n {\n \"text\": \"Data - Storage\",\n \"risk\": \"High\",\n \"id\": 39\n },\n {\n \"text\": \"Data - Processing\",\n \"risk\": \"High\",\n \"id\": 40\n },\n {\n \"text\": \"Tablet\",\n \"risk\": \"High\",\n \"id\": 41\n }\n ]\n },\n {\n \"id\": 2,\n \"question\": \"Please select the technology this device falls into\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"id\": 1,\n \"text\": \"Hardware - Access Control\"\n },\n {\n \"id\": 2,\n \"text\": \"Hardware - Air quality\"\n },\n {\n \"id\": 3,\n \"text\": \"Hardware - Asset location tracking\"\n },\n {\n \"id\": 4,\n \"text\": \"Hardware - Audio Visual\"\n },\n {\n \"id\": 5,\n \"text\": \"Hardware - Blinds/Facade\"\n },\n {\n \"id\": 6,\n \"text\": \"Hardware - Cameras\"\n },\n {\n \"id\": 7,\n \"text\": \"Hardware - Catering\"\n },\n {\n \"id\": 8,\n \"text\": \"Hardware - Data Ingestion/Managment\"\n },\n {\n \"id\": 9,\n \"text\": \"Hardware - EV Charging\"\n },\n {\n \"id\": 10,\n \"text\": \"Hardware - Fitness\"\n },\n {\n \"id\": 11,\n \"text\": \"Hardware - HVAC\"\n },\n {\n \"id\": 12,\n \"text\": \"Hardware - Irrigation\"\n },\n {\n \"id\": 13,\n \"text\": \"Hardware - Leak Detection\"\n },\n {\n \"id\": 14,\n \"text\": \"Hardware - Lifts/Elevators\"\n },\n {\n \"id\": 15,\n \"text\": \"Hardware - Lighting\"\n },\n {\n \"id\": 16,\n \"text\": \"Hardware - Metering\"\n },\n {\n \"id\": 17,\n \"text\": \"Hardware - Monitoring\"\n },\n {\n \"id\": 18,\n \"text\": \"Hardware - Occupancy\"\n },\n {\n \"id\": 19,\n \"text\": \"Hardware - System Integration\"\n },\n {\n \"id\": 20,\n \"text\": \"Hardware - Time Management\"\n },\n {\n \"id\": 21,\n \"text\": \"Hardware - UPS\"\n },\n {\n \"id\": 22,\n \"text\": \"Hardware - Waste Management\"\n },\n {\n \"id\": 23,\n \"text\": \"Building Automation\"\n },\n {\n \"id\": 24,\n \"text\": \"Other\"\n }\n ]\n },\n {\n \"id\": 3,\n \"question\": \"Does your device process any sensitive information? \",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"id\": 1,\n \"text\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"id\": 2,\n \"text\": \"No\",\n \"risk\": \"High\"\n },\n {\n \"id\": 3,\n \"text\": \"I don't know\",\n \"risk\": \"High\"\n }\n ]\n }\n ]\n },\n {\n \"step\": 2,\n \"title\": \"Tell us more about your device\",\n \"description\": \"Before your device can go through testing, tell us more about your device and its functionality. It is important that we fully understand your device before a thorough assessment can be made.\",\n \"questions\": [\n {\n \"id\": 1,\n \"question\": \"Can all non-essential services be disabled on your device?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Yes\",\n \"id\": 1\n },\n {\n \"text\": \"No\",\n \"id\": 2\n }\n ]\n },\n {\n \"id\": 2,\n \"question\": \"Is there a second IP port on the device?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Yes\",\n \"id\": 1\n },\n {\n \"text\": \"No\",\n \"id\": 2\n }\n ]\n },\n {\n \"id\": 3,\n \"question\": \"Can the second IP port on your device be disabled?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Yes\",\n \"id\": 1\n },\n {\n \"text\": \"No\",\n \"id\": 2\n },\n {\n \"text\": \"N/A\",\n \"id\": 3\n }\n ]\n }\n ]\n }\n]" + } + ] + }, + { + "name": "Get System Modules", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/modules", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "modules" + ] + }, + "description": "Get a list of all root CA certificates that have been loaded into Testrun" + }, + "response": [ + { + "name": "Get System Modules (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/modules", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "modules" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[\n \"Protocol\",\n \"Services\",\n \"Connection\",\n \"TLS\",\n \"NTP\",\n \"DNS\"\n]" + } + ] + }, + { + "name": "Get System Testpacks", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/testpacks", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "testpacks" + ] + }, + "description": "Obtain a list of applicable and available network adapters for use in testing" + }, + "response": [ + { + "name": "System Testpacks (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/testpacks", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "testpacks" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[\n \"Pilot Assessment\",\n \"Device Qualification\"\n]" + } + ] + }, + { + "name": "List Certificates", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + }, + "description": "Get a list of all root CA certificates that have been loaded into Testrun" + }, + "response": [ + { + "name": "List Certificates (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[\n {\n \"name\": \"GTS Root R1\",\n \"status\": \"Valid\",\n \"organisation\": \"Google Trust Services LLC\",\n \"expires\": \"2036-06-22T00:00:00+00:00\",\n \"filename\": \"crt.pem\"\n },\n {\n \"name\": \"WR2\",\n \"status\": \"Valid\",\n \"organisation\": \"Google Trust Services LLC\",\n \"expires\": \"2029-02-20T14:00:00+00:00\",\n \"filename\": \"WR2.pem\"\n }\n]" + }, + { + "name": "No Certificates (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[]" + } + ] + }, + { + "name": "Upload Certificate", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "NH1mNzqEQ/1c3.pem" + } + ] + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + }, + "description": "Upload a new root CA certificate into Testrun" + }, + "response": [ + { + "name": "Upload Certificate (201)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "NH1mNzqEQ/1c3.pem" + } + ] + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"name\": \"GTS Root R1\",\n \"status\": \"Valid\",\n \"organisation\": \"Google Trust Services LLC\",\n \"expires\": \"2036-06-22T00:00:00+00:00\",\n \"filename\": \"crt.pem\"\n}" + }, + { + "name": "Duplicate (409)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "NH1mNzqEQ/1c3.pem" + } + ] + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Conflict", + "code": 409, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A certificate with that file name already exists.\"\n}" + }, + { + "name": "Invalid Cert (400)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "postman-cloud:///1ef5fb1a-6342-44e0-b35f-700d496e0cc5" + } + ] + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Failed to upload certificate. Is it in the correct format?\"\n}" + } + ] + }, + { + "name": "Delete Certificate", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"GTS CA 1C3\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + }, + "description": "Delete a root CA certificate" + }, + "response": [ + { + "name": "Delete Certificate (200)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"GTS Root R1\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully deleted the certificate\"\n}" + }, + { + "name": "Not Found (404)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"non-existing name\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A certificate with that name could not be found\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Received a bad request\"\n}" + } + ] + }, + { + "name": "Get Profile Format", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profiles/format", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles", + "format" + ] + }, + "description": "Obtain the current format of the risk assessment questionnaire" + }, + "response": [ + { + "name": "Get Profile Format (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profiles/format", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles", + "format" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "cookie": [], + "body": "[\n {\n \"question\": \"How will this device be used at Google?\",\n \"description\": \"Desribe your use case. Add links to user journey diagrams and TDD if available.\",\n \"type\": \"text-long\",\n \"validation\": {\n \"max\": \"512\",\n \"required\": true\n }\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"description\": \"A manufacturer or supplier is considered third party in this case\",\n \"type\": \"select\",\n \"options\": [\n \"Google\",\n \"Third Party\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"type\": \"select\",\n \"options\": [\n \"Yes\",\n \"No\",\n \"N/A\"\n ],\n \"default\": \"N/A\",\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Data Transmission\",\n \"question\": \"Which of the following statements are true about this device?\",\n \"description\": \"This tells us about the types of data that are transmitted from this device and how the transmission is performed from a technical standpoint.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"PII/PHI, confidential/sensitive business data, Intellectual Property and Trade Secrets, Critical Infrastructure and Identity Assets to a domain outside Alphabet's ownership\",\n \"Data transmission occurs across less-trusted networks (e.g. the internet).\",\n \"A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)\",\n \"A confidentiality breach during transmission would have a substantial negative impact\",\n \"The device does not encrypt data during transmission\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Data Transmission\",\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"type\": \"select\",\n \"options\": [\n \"Yes\",\n \"No\",\n \"I don't know\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Remote Operation\",\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"description\": \"This tells us about how this device is managed remotely.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"PII/PHI, or confidential business data is accessible from the device without authentication\",\n \"Unrecoverable actions (e.g. disk wipe) can be performed remotely\",\n \"Authentication is not required for remote access\",\n \"The management interface is accessible from the public internet\",\n \"Static credentials are used for administration\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Operating Environment\",\n \"question\": \"Are any of the following statements true about this device?\",\n \"description\": \"This informs us about what other systems and processes this device is a part of.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"The device monitors an environment for active risks to human life.\",\n \"The device is used to convey people, or critical property.\",\n \"The device controls robotics in human-accessible spaces.\",\n \"The device controls physical access systems.\",\n \"The device is involved in processes required by regulations, or compliance. (ex. privacy, security, safety regulations)\",\n \"The device's failure would cause faults in other high-criticality processes.\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"question\": \"Comments\",\n \"description\": \"Anything else to share?\",\n \"type\": \"text-long\",\n \"validation\": {\n \"max\": \"512\"\n }\n }\n]" + } + ] + }, + { + "name": "List Profiles", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + }, + "description": "Get a list of risk assessment profiles saved by the user" + }, + "response": [ + { + "name": "List Profiles (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "cookie": [], + "body": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]" + }, + { + "name": "No Profiles (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "cookie": [], + "body": "[\n \n]" + } + ] + }, + { + "name": "Export Profile", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8000/profile/{profile_name}", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "{profile_name}" + ] + }, + "description": "Get the PDF report for a specific device and timestamp" + }, + "response": [ + { + "name": "Get Profile No Device (200)", + "originalRequest": { + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8000/profile/Test", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "Test" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "application/pdf", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "profile.pdf" + }, + { + "name": "Get Profile with Device (200))", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"mac_addr\": \"00:1e:42:35:73:c4\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profile/Test", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "Test" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "application/pdf", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "profile.pdf" + }, + { + "name": "Profile Not Found (404)", + "originalRequest": { + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8000/profile/NonExistingProfile", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "NonExistingProfile" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Profile could not be found\"\n}" + }, + { + "name": "Device Not Found (404)", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"mac_addr\": \"non_existing_mac_addr\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profile/Test", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "Test" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A device with that mac address could not be found\"\n}" + }, + { + "name": "Internal Server Error (500) Copy", + "originalRequest": { + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8000/profile/Test", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profile", + "Test" + ] + } + }, + "status": "Internal Server Error", + "code": 500, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Error retrieving the profile PDF\"\n}" + } + ] + }, + { + "name": "Update Profile", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Testing\",\n \"status\": \"Valid\",\n \"questions\": [\n {\n \"question\": \"What type of device is this?\",\n \"answer\": \"IoT Gateway\"\n },\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"asdasd\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"N/A\"\n },\n {\n \"question\": \"Are any of the following statements true about your device?\",\n \"answer\": [\n 3\n ]\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 5\n ]\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 5\n ]\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 6\n ]\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + }, + "description": "Create or update a risk assessment questionnaire response" + }, + "response": [ + { + "name": "Update Profile (200)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "\n{\n \"name\": \"New Profile\",\n \"rename\": \"Updated New Profile\",\n \"version\": \"2.0.1\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully updated that profile\"\n}" + }, + { + "name": "Create Profile (201)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "\n{\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully created a new profile\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid request received\"\n}" + }, + { + "name": "Not Implemented (501)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Not Implemented", + "code": 501, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Risk profiles are not available right now\"\n}" + } + ] + }, + { + "name": "Delete Profile", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"New Profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + }, + "description": "Delete an existing risk assessment questionaire response" + }, + "response": [ + { + "name": "Delete Profile (200)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"New Profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully deleted that profile\"\n}" + }, + { + "name": "Not Found (404)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"non-existing profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A profile with that name could not be found\"\n}" + }, + { + "name": "Internal Serves Error (500)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"non-existing profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Internal Server Error", + "code": 500, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"An error occurred whilst deleting that profile\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid request received\"\n}" + } + ] + }, + { + "name": "http://localhost:8000/profile/Test", "request": { - "method": "GET", - "header": [], + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"mac_addr\": \"non_existing_mac_addr\"}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "http://localhost:8000/system/version", + "raw": "http://localhost:8000/profile/Test", "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "system", - "version" + "profile", + "Test" ] - } + }, + "description": "Delete a root CA certificate" }, - "response": [] + "response": [ + { + "name": "Delete Certificate (200)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"GTS Root R1\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully deleted the certificate\"\n}" + }, + { + "name": "Not Found (404)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"non-existing name\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A certificate with that name could not be found\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Received a bad request\"\n}" + } + ] }, { - "name": "Get Report", + "name": "Get Profile Format", "request": { "method": "GET", "header": [], "url": { - "raw": "http://localhost:8000/report/{device_name}/{timestamp}", + "raw": "http://localhost:8000/profiles/format", "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "report", - "{device_name}", - "{timestamp}" + "profiles", + "format" ] - } + }, + "description": "Obtain the current format of the risk assessment questionnaire" }, - "response": [] + "response": [ + { + "name": "Get Profile Format (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profiles/format", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles", + "format" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "cookie": [], + "body": "[\n {\n \"question\": \"How will this device be used at Google?\",\n \"description\": \"Desribe your use case. Add links to user journey diagrams and TDD if available.\",\n \"type\": \"text-long\",\n \"validation\": {\n \"max\": \"512\",\n \"required\": true\n }\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"description\": \"A manufacturer or supplier is considered third party in this case\",\n \"type\": \"select\",\n \"options\": [\n \"Google\",\n \"Third Party\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"type\": \"select\",\n \"options\": [\n \"Yes\",\n \"No\",\n \"N/A\"\n ],\n \"default\": \"N/A\",\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Data Transmission\",\n \"question\": \"Which of the following statements are true about this device?\",\n \"description\": \"This tells us about the types of data that are transmitted from this device and how the transmission is performed from a technical standpoint.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"PII/PHI, confidential/sensitive business data, Intellectual Property and Trade Secrets, Critical Infrastructure and Identity Assets to a domain outside Alphabet's ownership\",\n \"Data transmission occurs across less-trusted networks (e.g. the internet).\",\n \"A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)\",\n \"A confidentiality breach during transmission would have a substantial negative impact\",\n \"The device does not encrypt data during transmission\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Data Transmission\",\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"type\": \"select\",\n \"options\": [\n \"Yes\",\n \"No\",\n \"I don't know\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Remote Operation\",\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"description\": \"This tells us about how this device is managed remotely.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"PII/PHI, or confidential business data is accessible from the device without authentication\",\n \"Unrecoverable actions (e.g. disk wipe) can be performed remotely\",\n \"Authentication is not required for remote access\",\n \"The management interface is accessible from the public internet\",\n \"Static credentials are used for administration\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Operating Environment\",\n \"question\": \"Are any of the following statements true about this device?\",\n \"description\": \"This informs us about what other systems and processes this device is a part of.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"The device monitors an environment for active risks to human life.\",\n \"The device is used to convey people, or critical property.\",\n \"The device controls robotics in human-accessible spaces.\",\n \"The device controls physical access systems.\",\n \"The device is involved in processes required by regulations, or compliance. (ex. privacy, security, safety regulations)\",\n \"The device's failure would cause faults in other high-criticality processes.\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"question\": \"Comments\",\n \"description\": \"Anything else to share?\",\n \"type\": \"text-long\",\n \"validation\": {\n \"max\": \"512\"\n }\n }\n]" + } + ] }, { - "name": "Get Export", + "name": "List Profiles", "request": { "method": "GET", "header": [], "url": { - "raw": "http://localhost:8000/export/{device_name}/{timestamp}", + "raw": "http://localhost:8000/profiles", "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "export", - "{device_name}", - "{timestamp}" + "profiles" ] - } + }, + "description": "Get a list of risk assessment profiles saved by the user" }, - "response": [] - }, - { - "name": "List Certificates", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "http://localhost:8000/system/config/certs/list", - "protocol": "http", - "host": [ - "localhost" + "response": [ + { + "name": "List Profiles (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } ], - "port": "8000", - "path": [ - "system", - "config", - "certs", - "list" - ] + "cookie": [], + "body": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]" + }, + { + "name": "No Profiles (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "cookie": [], + "body": "[\n \n]" } - }, - "response": [] + ] }, { - "name": "Upload Certificate", + "name": "Update Profile", "request": { "method": "POST", "header": [], "body": { - "mode": "file", - "file": {} + "mode": "raw", + "raw": "{\n \"name\": \"Testing\",\n \"status\": \"Valid\",\n \"questions\": [\n {\n \"question\": \"What type of device is this?\",\n \"answer\": \"IoT Gateway\"\n },\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"asdasd\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"N/A\"\n },\n {\n \"question\": \"Are any of the following statements true about your device?\",\n \"answer\": [\n 3\n ]\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 5\n ]\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 5\n ]\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 6\n ]\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } }, "url": { - "raw": "http://localhost:8000/system/config/certs/upload", + "raw": "http://localhost:8000/profiles", "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "system", - "config", - "certs", - "upload" + "profiles" ] - } + }, + "description": "Create or update a risk assessment questionnaire response" }, - "response": [] + "response": [ + { + "name": "Update Profile (200)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully updated that profile\"\n}" + }, + { + "name": "Create Profile (201)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully created a new profile\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid request received\"\n}" + }, + { + "name": "Not Implemented (501)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Not Implemented", + "code": 501, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Risk profiles are not available right now\"\n}" + } + ] }, { - "name": "Delete Certificate", + "name": "Delete Profile", "request": { "method": "DELETE", "header": [], "body": { "mode": "raw", - "raw": "{\n \"name\": \"iot.bms.google.com\"\n}", + "raw": "{\n \"name\": \"New Profile\"\n}", "options": { "raw": { "language": "json" @@ -1043,21 +4063,184 @@ } }, "url": { - "raw": "http://localhost:8000/system/config/certs/delete", + "raw": "http://localhost:8000/profiles", "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "system", - "config", - "certs", - "delete" + "profiles" ] - } + }, + "description": "Delete an existing risk assessment questionaire response" }, - "response": [] + "response": [ + { + "name": "Delete Profile (200)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"New Profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully deleted that profile\"\n}" + }, + { + "name": "Not Found (404)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"non-existing profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A profile with that name could not be found\"\n}" + }, + { + "name": "Internal Serves Error (500)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"non-existing profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Internal Server Error", + "code": 500, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"An error occurred whilst deleting that profile\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid request received\"\n}" + } + ] } ] } \ No newline at end of file diff --git a/docs/get_started.md b/docs/get_started.md index f9a2aa2f8..41a1d13f8 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -1,123 +1,144 @@ Testrun logo +# Get started -## Getting Started +This page covers the following topics: -It is recommended that you run Testrun on a standalone machine running a fresh install of Ubuntu 20.04, 22.04 or 24.04 LTS (laptop or desktop). +- [Get started](#get-started) +- [Prerequisites](#prerequisites) + - [Hardware](#hardware) + - [Software](#software) + - [Device](#device) +- [Installation](#installation) +- [Testing](#testing) + - [Start Testrun](#start-testrun) + - [Test your device](#test-your-device) +- [Additional Configuration Options](/docs/additional_config.md) +- [Troubleshooting](#troubleshooting) +- [Review the report](#review-the-report) +- [Uninstall](#uninstall) -## Prerequisites +# Prerequisites -### Hardware +We recommend that you run Testrun on a stand-alone machine that has a fresh install of Ubuntu 22.04 or 24.04 LTS (laptop or desktop). -Before starting with Testrun, ensure you have the following hardware: +## Hardware -- PC running Ubuntu LTS (laptop or desktop) -- 2x USB Ethernet adapter (one may be a built-in Ethernet port) -- Internet connection +Before you start, ensure you have the following hardware: -![Visual representation of setup](setup/visual.png) +- PC running Ubuntu 22.04 or 24.04 LTS (laptop or desktop) +- 2x ethernet ports (USB ethernet adapters work too) +- Internet connection -**NOTE: Running in a virtual machine? Checkout the virtual machine documentation [here](/docs/virtual_machine.md).** +![Required hardware for Testrun](/docs/ui/getstarted--2dn8vrzsspe.png) -### Software +Note: If you're using Testrun in a virtual machine, follow the steps on the [Virtual machine page](/docs/virtual_machine.md). -Ensure the following software is installed on your Ubuntu LTS PC: -- Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) -- System dependencies (These will be installed automatically when installing Testrun if not already installed): - - Python3-dev - - Python3-venv - - Openvswitch Common - - Openvswitch Switch - - Build Essential - - Net Tools - - Ethtool +## Software -### Device -Any device with an ethernet connection, and support for IPv4 DHCP can be tested. +Install Docker on your Ubuntu LTS PC using [the installation guide](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository). The following system dependencies install automatically when you install Testrun: -However, to achieve a compliant test outcome, your device must be configured correctly and implement the required security features. These standards are outlined in the [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements). but further detail is available in [documentation for each test module](/docs/test/modules.md). +- Python3-dev +- Python3-venv +- Openvswitch Common +- Openvswitch Switch +- Build Essential +- Net Tools +- Ethtool -## Installation +## Device -1. Download the latest version of the Testrun installer from the [releases page](https://github.com/google/testrun/releases) +You can test any device with an Ethernet connection and support for IPv4 DHCP. However, to achieve a compliant test outcome, you must configure your device correctly and implement the required security features. The [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements) outlines these standards. Additional details are available on the [Test modules page](/docs/test/modules.md). -2. Open a terminal and navigate to location of the Testrun installer (most likely your downloads folder) +# Installation -3. Install the package using ``sudo apt install ./testrun*.deb`` +Follow these steps to install Testrun: - - Testrun will be installed under the /usr/local/testrun directory. - - Testing data will be available in the ``local/devices/{device}/reports`` folders +1. Download the latest version of the Testrun installer from the [Releases page](https://github.com/google/testrun/releases). +2. Open a terminal and navigate to the location of the Testrun installer (most likely your Downloads folder). +3. Install the package using `sudo apt install ./testrun*.deb` - **NOTE: Local CA certificates should be uploaded within Testrun to run TLS server testing** +Testrun installs under the `/usr/local/testrun` directory. Testing data is available in the `local/devices/{device}/reports` folders. - ![Installing Testrun in terminal window](setup/install.gif) +![Terminal during install](/docs/setup/install.gif) -## Start Testrun - -1. Attach network interfaces: - - Connect one USB Ethernet adapter to the internet source (e.g., router or switch) using an ethernet cable. - - Connect the other USB Ethernet adapter directly to the IoT device you want to test using an ethernet cable. - - Some things to remember: - - Both adapters should be disabled in the host system (IPv4, IPv6 and general). You can do this by going to Settings > Network - - The device under test should be powered off until prompted - - Struggling to identify the correct interfaces? See [this guide](network/identify_interfaces.md). - -2. Start Testrun. - -Start Testrun with the command `sudo testrun` - - - To run Testrun in network-only mode (without running any tests), use the `--net-only` option. - - - To run Testrun with just one interface (connected to the device), use the ``--single-intf`` option. - -## Test Your Device - -1. Once Testrun has started, open your browser to http://localhost:8080. - -2. Configure your network interfaces under the settings menu - located in the top right corner of the application. Settings can be changed at any time. - - ![](/docs/ui/settings_icon.png) - -3. Navigate to the device repository icon to add a new device for testing. - - ![](/docs/ui/device_icon.png) +# Testing -4. Click the button 'Add Device'. - -5. Enter the MAC address, manufacturer name and model number. - -6. Select the test modules you wish to enable for this device (Hint: All are required for qualification purposes) and click save. - -7. Navigate to the Testrun progress icon and click the button 'Start New Testrun'. - - ![](/docs/ui/progress_icon.png) - -8. Select the device you would like to test. - -9. Enter the version number of the firmware running on the device. - -10. Click 'Start Testrun' - - - During testing, if you would like to stop Testrun, click 'Stop' next to the test name. +## Start Testrun -11. Once the notification 'Waiting for Device' appears, power on the device under test. +Follow these steps to start Testrun: +1. Attach the network interfaces. + - Connect one USB Ethernet adapter to the internet source (e.g., router or switch) using an Ethernet cable. + - Connect the other USB Ethernet adapter directly to the IoT device you want to test using an Ethernet cable. + + Notes: + + - Disable both adapters in the host system (IPv4, IPv6, and general) by opening **Settings**, then **Network**. + - Keep the DUT powered off until prompted. Otherwise, Testrun will not be able to fully capture the device behavior during startup - resulting in inaccurate test results. + +1. Start Testrun with the command `sudo testrun` + - To run Testrun in network-only mode (without running any tests), use the `--net-only` option. + - To run Testrun with just one interface (connected to the device), use the `--single-intf` option. + + **Note**: Tests that require an internet connection (e.g TLS client, DNS) will produce a non-compliant result. + +## Test your device + +Follow these steps to test your IoT device: + +1. Open Testrun by navigating to [http://localhost:8080](http://localhost:8080/) in your browser. +2. Select the **Settings** menu in the top-right corner. It will open the **General** tab by default. Then select your network interfaces. You can change the settings at any time. + ![Settings menu button](/docs/ui/getstarted-settings-menu.png) +3. Select the **Certificates** tab (Settings menu), then upload your local CA certificates for TLS server testing. + ![Certificates menu button](/docs/ui/getstarted-certificates-menu.png) +4. Select the **Device Repository** icon on the left panel to add a new device for testing. + ![Device repository button](/docs/ui/getstarted-device-repository.png) + + Or + + Click the **Actions** button to add a new device for testing. + ![Actions button](/docs/ui/getstarted-actions-device.png) +5. Select the **Add Device** button on the Device repository. + ![Add device](/docs/ui/getstarted-add-device.png) +6. Enter the MAC address, manufacturer name, and model number. +7. Select Qualification or Pilot program. +8. Select the test modules you want to enable for this device. +Note: For qualification purposes, you must select all. +9. Answer a few questions about your device. +10. Select **Save**. +11. Select the Testrun progress icon, then select the **Testing** button.![Testing button](/docs/ui/getstarted-testing.png) + + Or + + Click the **Actions** button, then select the **Start Testing** button. + ![Actions button](/docs/ui/getstarted-actions-testing.png) + +12. Select the device you want to test. +13. Enter the version number of the firmware running on the device. +14. Select **Start Testrun**. + - If you need to stop Testrun during testing, select **Stop** next to the test name. +15. Once the Waiting for Device notification appears, power on the device under test. + ![Waiting for device](/docs/ui/getstarted-waiting-for-device.png) +16. While testing is in progress, you could complete a Risk Assessment. To do so, go to the **Risk assessment** tab. + ![Risk assessment](/docs/ui/getstarted-risk-assessment.png) +17. A report appears under the Reports icon once the test sequence is complete. + ![Reports button](/docs/ui/getstarted-reports.png) -12. On completion of the test sequence, a report will appear under the history icon. +# Troubleshooting - ![](/docs/ui/history_icon.png) +If you encounter any issues, try the following: -# Troubleshooting +- Ensure that your computer meets all hardware and software prerequisites. +- Verify that the network interfaces are connected correctly. +- Check the configuration settings. +- Refer to the [Testrun documentation](/docs). -If you encounter any issues or need assistance, consider the following: +If you still need assistance, ask a question on the [Issues page](https://github.com/google/testrun/issues). -- Ensure that all hardware and software prerequisites are met. -- Verify that the network interfaces are connected correctly. -- Check the configuration settings. -- Refer to the Testrun documentation or ask for assistance in the issues page: https://github.com/google/testrun/issues +# Review the report -# Reviewing -Once you have completed a test attempt, you may want to review the test report provided by Testrun. For more information about what Testrun looks for when testing, and what the output means, take a look at the testing documentation: [Testing](/docs/test/README.md). +Once you complete a test attempt, you can review the test report provided by Testrun. For more information on Testrun requirements and outputs, refer to the [Testing documentation](/docs/test/README.md). # Uninstall -To uninstall Testrun, use the built-in dpkg uninstall command to remove Testrun correctly. For Testrun, this would be: ```sudo apt-get remove testrun```. + +To uninstall Testrun correctly, use the built-in dpkg uninstall command: `sudo apt-get remove testrun` diff --git a/docs/network/README.md b/docs/network/README.md index b5536c30c..0efb5e3ff 100644 --- a/docs/network/README.md +++ b/docs/network/README.md @@ -1,45 +1,41 @@ Testrun logo +# Network -## Network Overview +This page provides an overview of Testrun's network services. Visit these pages for additional information: -## Table of Contents -1) Network Overview (this page) -2) [How to identify network interfaces](identify_interfaces.md) -3) [Addresses](addresses.md) -4) [Add a new network service](add_new_service.md) +- [Network addresses](/docs/network/addresses.md) +- [Add a new network service](/docs/network/add_new_service.md) -Testrun provides several built-in network services that can be utilized for testing purposes. These services are already available and can be used without any additional configuration. +Testrun provides several built-in network services you can use for testing purposes. These services don't require any additional configuration. Below is a list and brief description of the network services provided. -The following network services are provided: - -### Internet Connectivity (Gateway Service) +# Internet connectivity (gateway service) The gateway service provides internet connectivity to the test network. It allows devices in the network to access external resources and communicate with the internet. -### DHCPv4 Service +# DHCPv4 service The DHCPv4 service provides Dynamic Host Configuration Protocol (DHCP) functionality for IPv4 addressing. It includes the following components: -- Primary DHCP Server: A primary DHCP server is available to assign IPv4 addresses to DHCP clients in the network. -- Secondary DHCP Server (Failover Configuration): A secondary DHCP server operates in failover configuration with the primary server to provide high availability and redundancy. +- Primary DHCP server: Assigns IPv4 addresses to DHCP clients in the network. +- Secondary DHCP server (failover configuration): Operates in failover configuration with the primary server to provide high availability and redundancy. -#### Configuration +## Configuration -The configuration of the DHCPv4 service can be modified using the provided GRPC (gRPC Remote Procedure Call) service. +You can modify the configuration of the DHCPv4 service using the provided Remote Procedure Call (GRPC) service. -### IPv6 SLAAC Addressing +# IPv6 SLAAC addressing -The primary DHCP server also provides IPv6 Stateless Address Autoconfiguration (SLAAC) addressing for devices in the network. IPv6 addresses are automatically assigned to devices using SLAAC where test devices support it. +The primary DHCP server provides IPv6 Stateless Address Autoconfiguration (SLAAC) addressing for devices on the network. It automatically assigns IPv6 addresses to devices using SLAAC where test devices support it. -### NTP Service +# NTP service -The Network Time Protocol (NTP) service provides time synchronization for devices in the network. It ensures that all devices have accurate and synchronized time information. +The Network Time Protocol (NTP) service provides time synchronization for devices on the network. It ensures that all devices have accurate and synchronized time information. -### DNS Service +# DNS service -The DNS (Domain Name System) service resolves domain names to their corresponding IP addresses. It allows devices in the network to access external resources using domain names. +The Domain Name System (DNS) service resolves domain names to their corresponding IP addresses. It allows devices on the network to access external resources using domain names. -### 802.1x Authentication (Radius Module) +# 802.1x authentication (radius module) -The radius module provides 802.1x authentication for devices in the network. It ensures secure and authenticated access to the network. The issuing CA (Certificate Authority) certificate can be specified by the user if required. \ No newline at end of file +The radius module provides 802.1x authentication for devices on the network. It ensures secure and authenticated access to the network. The user can specify the issuing Certificate Authority (CA) certificate if required. \ No newline at end of file diff --git a/docs/network/add_new_service.md b/docs/network/add_new_service.md index 7a07e43be..e6b01e102 100644 --- a/docs/network/add_new_service.md +++ b/docs/network/add_new_service.md @@ -1,21 +1,44 @@ Testrun logo -## Adding a New Network Service +# Add a new network service -The Testrun framework allows users to add their own network services with ease. A template network service can be used to get started quickly, this can be found at [modules/network/template](../../modules/network/template). Otherwise, see below for details of the requirements for new network services. +The Testrun framework allows you to easily add your own network services. You can use the template network service at [modules/network/template](/modules/network/template). To add a new network service, follow these steps: -To add a new network service to Testrun, follow the procedure below: +1. Create a folder under `modules/network/` with the name of the network service in lowercase using only alphanumeric characters and hyphens (-). +1. Include the following items in the created folder: + - `{module}.Dockerfile`: Dockerfile builds the network service image. Replace `{module}` with the name of the module. + - `conf/`: Folder containing the module configuration files. + - `bin/`: Folder containing the start-up script for the network service. + - Place any additional application code in its own folder. -1. Create a folder under `modules/network/` with the name of the network service in lowercase, using only alphanumeric characters and hyphens (`-`). -2. Inside the created folder, include the following files and folders: - - `{module}.Dockerfile`: Dockerfile for building the network service image. Replace `{module}` with the name of the module. - - `conf/`: Folder containing the module configuration files. - - `bin/`: Folder containing the startup script for the network service. - - Any additional application code can be placed in its own folder. +Here are some examples: -### Example `module_config.json` +## {module}.Dockerfile -```json +``` +# Image name: test-run/{module} +FROM test-run/base:latest + +ARG MODULE_NAME={module} +ARG MODULE_DIR=modules/network/$MODULE_NAME + +# Install network service dependencies +# ... + +# Copy over all configuration files +COPY $MODULE_DIR/conf /testrun/conf + +# Copy over all binary files +COPY $MODULE_DIR/bin /testrun/bin + +# Copy over all python files +COPY $MODULE_DIR/python /testrun/python + +# Do not specify a CMD or Entrypoint as Testrun will automatically start your service as required by calling the start_network_service script in the bin folder +``` + +## module_config.json +``` { "config": { "meta": { @@ -42,35 +65,11 @@ To add a new network service to Testrun, follow the procedure below: } } } -``` - -### Example of {module}.Dockerfile -```Dockerfile -# Image name: test-run/{module} -FROM test-run/base:latest - -ARG MODULE_NAME={module} -ARG MODULE_DIR=modules/network/$MODULE_NAME - -# Install network service dependencies -# ... - -# Copy over all configuration files -COPY $MODULE_DIR/conf /testrun/conf - -# Copy over all binary files -COPY $MODULE_DIR/bin /testrun/bin - -# Copy over all python files -COPY $MODULE_DIR/python /testrun/python - -# Do not specify a CMD or Entrypoint as Test Run will automatically start your service as required ``` -### Example of start_network_service script - -```bash +## start_network_service script +``` #!/bin/bash CONFIG_FILE=/etc/network_service/config.conf @@ -89,8 +88,4 @@ echo "Starting Network Service..." # Restart the network service when the config changes # ... -``` - - - - +``` \ No newline at end of file diff --git a/docs/network/addresses.md b/docs/network/addresses.md index 7fa71d716..261242687 100644 --- a/docs/network/addresses.md +++ b/docs/network/addresses.md @@ -1,20 +1,19 @@ Testrun logo -## Network Addresses +# Network addresses -Each network service is configured with an IPv4 and IPv6 address. For IPv4 addressing, the last number in the IPv4 address is fixed (ensuring the IP is unique). See below for a table of network addresses: +Each network service is configured with an IPv4 and IPv6 address. For IPv4 addressing, the last number in the IPv4 address is fixed, ensuring the IP is unique. The table below lists network addresses you might need. -| Name | Mac address | IPv4 address | IPv6 address | -|---------------------|----------------------|--------------|------------------------------| -| Internet gateway | 9a:02:57:1e:8f:01 | 10.10.10.1 | fd10:77be:4186::1 | -| DHCP primary | 9a:02:57:1e:8f:02 | 10.10.10.2 | fd10:77be:4186::2 | -| DHCP secondary | 9a:02:57:1e:8f:03 | 10.10.10.3 | fd10:77be:4186::3 | -| DNS server | 9a:02:57:1e:8f:04 | 10.10.10.4 | fd10:77be:4186::4 | -| NTP server | 9a:02:57:1e:8f:05 | 10.10.10.5 | fd10:77be:4186::5 | -| Radius authenticator| 9a:02:57:1e:8f:07 | 10.10.10.7 | fd10:77be:4186::7 | -| Active test module | 9a:02:57:1e:8f:09 | 10.10.10.9 | fd10:77be:4186::9 | +| Name | MAC address | IPv4 address | IPv6 address | +| ----------------- | ----------------- | ------------- | ------------------- | +| Internet gateway | 9a\:02\:57\:1e\:8f\:01 | 10.10.10.1 | fd10\:77be\:4186\:\:1 | +| DHCP primary | 9a\:02\:57\:1e\:8f\:02 | 10.10.10.2 | fd10\:77be\:4186\:\:2 | +| DHCP secondary | 9a\:02\:57\:1e\:8f\:03 | 10.10.10.3 | fd10\:77be\:4186\:\:3 | +| DNS server | 9a\:02\:57\:1e\:8f\:04 | 10.10.10.4 | fd10\:77be\:4186\:\:4 | +| NTP server | 9a\:02\:57\:1e\:8f\:05 | 10.10.10.5 | fd10\:77be\:4186\:\:5 | +| Radius authenticator | 9a\:02\:57\:1e\:8f\:07 | 10.10.10.7 | fd10\:77be\:4186\:\:7 | +| Active test module | 9a\:02\:57\:1e\:8f\:09 | 10.10.10.9 | fd10\:77be\:4186\:\:9 | +The default network range is 10.10.10.0/24 and devices are assigned addresses in that range via DHCP. The range may change when requested by a test module. In that case, network services restart and are accessible on the new range with the same final host ID. The default IPv6 network is fd10:77be:4186::/64 and addresses are assigned to devices on the network using IPv6 SLAAC. -The default network range is 10.10.10.0/24 and devices will be assigned addresses in that range via DHCP. The range may change when requested by a test module. In which case, network services will be restarted and accessible on the new range, with the same final host ID. The default IPv6 network is fd10:77be:4186::/64 and addresses will be assigned to devices on the network using IPv6 SLAAC. - -When creating a new network module, please ensure that the ip_index value in the module_config.json is unique otherwise unexpected behaviour will occur. \ No newline at end of file +When creating a new network module, ensure that the ip_index value in the module_config.json is unique to prevent unexpected behavior. \ No newline at end of file diff --git a/docs/network/identify_interfaces.md b/docs/network/identify_interfaces.md deleted file mode 100644 index 50e62acd3..000000000 --- a/docs/network/identify_interfaces.md +++ /dev/null @@ -1,20 +0,0 @@ -Testrun logo - -## Identifying network interfaces - -For Testrun to operate correctly, you must select the correct network interfaces within the settings panel of the user interface. There are 2 methods to identify the correct network interfaces: - -A) Find the printed MAC address on your interface - -Some USB network interfaces will have the MAC address printed on the interface itself. This will look something like: ```00:e0:4c:02:0f:a8```. - -Compare this printed MAC address against the MAC address provided in the settings panel in the user interface. - -B) Connect your interfaces one at a time - - 1) Ensure both interfaces are disconnected from your PC and open the settings panel in the user interface. - - 2) Connect your internet interface to your PC and refresh the settings panel. One interface, which was not previously present, should now be visibile. - - 3) Repeat the previous step for the devices interface. - diff --git a/docs/roadmap.pdf b/docs/roadmap.pdf index 9f4599ee1..eaaf0ea64 100644 Binary files a/docs/roadmap.pdf and b/docs/roadmap.pdf differ diff --git a/docs/test/README.md b/docs/test/README.md index 19aa691d8..19fcf3fd5 100644 --- a/docs/test/README.md +++ b/docs/test/README.md @@ -1,8 +1,5 @@ Testrun logo +# Testing -## Testing - -The test requirements that are investigated by Testrun can be found in the [test modules documentation](/docs/test/modules.md). - -To understand the testing results, various definitions of test results and requirements are specified in the [statuses documentation](/docs/test/statuses.md). \ No newline at end of file +Testrun provides modules for you to test your own device. You can learn more about the requirements for each on the [Test modules page](/docs/test/modules.md). The [Test results page](/docs/test/statuses.md) outlines possible results and how to interpret them. diff --git a/docs/test/modules.md b/docs/test/modules.md index 7c5851ba4..ff6bb5c53 100644 --- a/docs/test/modules.md +++ b/docs/test/modules.md @@ -1,16 +1,26 @@ Testrun logo -## Test Modules +# Test modules -Testrun provides some pre-built test modules for you to use when testing your own device. These test modules are listed below: +Testrun provides some pre-built modules you can use when testing your own device. The table below lists the test module, its purpose, and a link to additional information. -| Name | Description | Read more | -|---|---|---| -| Base | Template for all test modules | [Base module](/modules/test/base/README.md) | -| Baseline | A sample test module | [Baseline module](/modules/test/baseline/README.md) | -| Connection | Verify IP and DHCP based behavior | [Connection module](/modules/test/conn/README.md) | -| DNS | Verify DNS functionality | [DNS module](/modules/test/dns/README.md) | -| NMAP | Ensure unsecure services are disabled | [NMAP module](/modules/test/nmap/README.md) | -| NTP | Verify NTP functionality | [NTP module](/modules/test/ntp/README.md) | -| Protocol | Inspect BMS protocol implementation | [Protocol Module](/modules/test/protocol/README.md) | -| TLS | Determine TLS client and server behavior | [TLS module](/modules/test/tls/README.md) | +| Module name | Purpose | Additional documentation | +| ------------ | ---------------------------- | ----------------------------- | +| Base | Template for all test modules | [Base module] | +| Baseline | Sample test module | [Baseline module] | +| Connection | Verify IP and DHCP-based behavior | [Connection module] | +| DNS | Verify DNS functionality | [DNS module] | +| Services | Ensure unsecure services are disabled | [Services module] | +| NTP | Verify NTP functionality | [NTP module] | +| Protocol | Inspect BMS protocol implementation | [Protocol Module] | +| TLS | Determine TLS client and server behavior | [TLS module] | + + +[Base module]: /modules/test/base/README.md +[Baseline module]: /modules/test/baseline/README.md +[Connection module]: /modules/test/conn/README.md +[DNS module]: /modules/test/dns/README.md +[Services module]: /modules/test/services/README.md +[NTP module]: /modules/test/ntp/README.md +[Protocol Module]: /modules/test/protocol/README.md +[TLS module]: /modules/test/tls/README.md diff --git a/docs/test/statuses.md b/docs/test/statuses.md index d196fa4af..8268b3e88 100644 --- a/docs/test/statuses.md +++ b/docs/test/statuses.md @@ -1,33 +1,33 @@ Testrun logo -## Test Statuses -Testrun will output the result and description of each automated test. The test results will be one of the following: +# Test results -| Name | Description | What next? | -|---|---|---| -| Compliant | The device implements the required feature correctly | Nothing | -| Non-Compliant | The device does not support the specified requirements for the test | Modify or implement the required functionality on the device | -| Feature Not Detected | The device does not implement a feature covered by the test | You may implement the functionality (not required) | -| Error | An error occured whilst running the test | Create a bug report requesting additional support to diagnose the issue | +Testrun outputs the result and a description of each automated test. The table below includes the result name, its description, and what your next step should be. -## Test Requirement -Testrun also determines whether each test is required for the device to receive an overall compliant result. These rules are: +| Result name | Description | What next? | +| --------------------- | ------------------------ | ------------------------ | +| Compliant | The device implements the required feature correctly. | Nothing. | +| Non-Compliant | The device doesn’t support the specified requirements for the test. | Modify or implement the required functionality on the device. | +| Informational | Extra information about the device under test | Nothing. | +| Feature Not Detected | The device doesn’t implement a feature covered by the test. | You may implement the functionality but it’s not required. | +| Error | An error occurred while running the test. | Create a bug report requesting additional support to diagnose the issue. | -| Name | Description | -|---|---| -| Required | The device must implement the feature | -| Recommended | The device should implement the feature, but will not receive an overall Non-Compliant if not implemented | -| Roadmap | The device should implement this feature in the future, but is not required at the moment | -| Required If Applicable | If the device implements this feature, it must be implemented correctly (as per the test requirements) | -## Testrun Statuses -Once testing is completed, an overall result for the test attempt will be produced. This is calculated by comparing the result of all tests, and whether they are required or not required. +# Test requirements -### Compliant -All required tests are implemented correctly, and all required if applicable tests are implemented correctly (where the feature has been implemented). +Testrun determines whether the device needs each test to receive an overall compliant result. Here are the rules and what they mean: -### Non-Compliant -One or more of the required tests (or required if applicable tests) have produced a non-compliant result. +- Required: The device must implement the feature. +- Recommended: The device should implement the feature but won't receive an overall Non-Compliant if it's not implemented. +- Roadmap: The device should implement this feature in the future, but it's not required at the moment. +- Required If Applicable: If the device implements this feature, it must be implemented correctly (per the test requirements). +- Informational: Regardless whether the device implements the feature or not, the test will receive an Informational result. -### Error -One of more of the required tests (or required if applicable tests) have not executed correctly. This does not necessarily indicate that the device is compliant or non-compliant. +# Testrun statuses + +Once testing is complete, the program produces an overall status for the test attempt. It's calculated by comparing the results of all tests and whether they're required or not. The possible statuses are: + +- Compliant: All required tests are implemented correctly, and all required if applicable tests are implemented correctly (where the feature is implemented). +- Non-Compliant: One or more of the required tests (or Required If Applicable tests) produced a Non-Compliant result. +- Error: One or more of the required tests (or Required If Applicable tests) didn't execute correctly. This doesn't necessarily indicate that the device is Compliant or Non-Compliant. +- Cancelled: Either the device was disconnected during testing or the user requested to cancel the test attempt. \ No newline at end of file diff --git a/docs/ui/accessibility.md b/docs/ui/accessibility.md new file mode 100644 index 000000000..58d948522 --- /dev/null +++ b/docs/ui/accessibility.md @@ -0,0 +1,12 @@ +Testrun logo + +# Accessibility + +We designed Testrun with accessibility at its core. The application provides full support for: + +- Screen readers +- Keyboard navigation +- Responsive resizing and scaling +- [Helperbird](https://www.helperbird.com/) + +For a more thorough explanation of these features, download the [accessibility video](https://github.com/google/testrun/raw/main/docs/ui/accessibility.mp4). If you require further accommodations when using Testrun, please [raise an issue](https://github.com/google/testrun/issues/new/choose). \ No newline at end of file diff --git a/docs/ui/device_icon.png b/docs/ui/device_icon.png deleted file mode 100644 index 2472f7da2..000000000 Binary files a/docs/ui/device_icon.png and /dev/null differ diff --git a/docs/ui/getstarted--2dn8vrzsspe.png b/docs/ui/getstarted--2dn8vrzsspe.png new file mode 100644 index 000000000..302e55302 Binary files /dev/null and b/docs/ui/getstarted--2dn8vrzsspe.png differ diff --git a/docs/ui/getstarted--3d9k3si3ul1.png b/docs/ui/getstarted--3d9k3si3ul1.png new file mode 100644 index 000000000..5c762156d Binary files /dev/null and b/docs/ui/getstarted--3d9k3si3ul1.png differ diff --git a/docs/ui/getstarted-actions-device.png b/docs/ui/getstarted-actions-device.png new file mode 100644 index 000000000..4363a194f Binary files /dev/null and b/docs/ui/getstarted-actions-device.png differ diff --git a/docs/ui/getstarted-actions-testing.png b/docs/ui/getstarted-actions-testing.png new file mode 100644 index 000000000..dbffebb14 Binary files /dev/null and b/docs/ui/getstarted-actions-testing.png differ diff --git a/docs/ui/getstarted-add-device.png b/docs/ui/getstarted-add-device.png new file mode 100644 index 000000000..5b93b46b4 Binary files /dev/null and b/docs/ui/getstarted-add-device.png differ diff --git a/docs/ui/getstarted-certificates-menu.png b/docs/ui/getstarted-certificates-menu.png new file mode 100644 index 000000000..60eceae7a Binary files /dev/null and b/docs/ui/getstarted-certificates-menu.png differ diff --git a/docs/ui/getstarted-device-repository.png b/docs/ui/getstarted-device-repository.png new file mode 100644 index 000000000..236842ce4 Binary files /dev/null and b/docs/ui/getstarted-device-repository.png differ diff --git a/docs/ui/getstarted-reports.png b/docs/ui/getstarted-reports.png new file mode 100644 index 000000000..17577b735 Binary files /dev/null and b/docs/ui/getstarted-reports.png differ diff --git a/docs/ui/getstarted-risk-assessment.png b/docs/ui/getstarted-risk-assessment.png new file mode 100644 index 000000000..a14b7da10 Binary files /dev/null and b/docs/ui/getstarted-risk-assessment.png differ diff --git a/docs/ui/getstarted-settings-menu.png b/docs/ui/getstarted-settings-menu.png new file mode 100644 index 000000000..6c53ed4ec Binary files /dev/null and b/docs/ui/getstarted-settings-menu.png differ diff --git a/docs/ui/getstarted-testing.png b/docs/ui/getstarted-testing.png new file mode 100644 index 000000000..634269e5c Binary files /dev/null and b/docs/ui/getstarted-testing.png differ diff --git a/docs/ui/getstarted-waiting-for-device.png b/docs/ui/getstarted-waiting-for-device.png new file mode 100644 index 000000000..36a307316 Binary files /dev/null and b/docs/ui/getstarted-waiting-for-device.png differ diff --git a/docs/ui/history_icon.png b/docs/ui/history_icon.png deleted file mode 100644 index eb95a8663..000000000 Binary files a/docs/ui/history_icon.png and /dev/null differ diff --git a/docs/ui/progress_icon.png b/docs/ui/progress_icon.png deleted file mode 100644 index c326d185e..000000000 Binary files a/docs/ui/progress_icon.png and /dev/null differ diff --git a/docs/ui/settings_icon.png b/docs/ui/settings_icon.png deleted file mode 100644 index 8fc83b9bb..000000000 Binary files a/docs/ui/settings_icon.png and /dev/null differ diff --git a/docs/ui/settings_menu.png b/docs/ui/settings_menu.png deleted file mode 100644 index 046526b25..000000000 Binary files a/docs/ui/settings_menu.png and /dev/null differ diff --git a/docs/ui/test_name.png b/docs/ui/test_name.png deleted file mode 100644 index 3d18df19d..000000000 Binary files a/docs/ui/test_name.png and /dev/null differ diff --git a/docs/virtual_machine.md b/docs/virtual_machine.md index 2f029b296..827ee36b3 100644 --- a/docs/virtual_machine.md +++ b/docs/virtual_machine.md @@ -1,38 +1,41 @@ Testrun logo -## Virtual Machine +# Run on a virtual machine -This guide will provide steps to use Testrun within a virtual machine in virtual Box (VMWare and other providers have not yet been tested). You should use this guide alongside the [Get Started guide](/docs/get_started.md) - only differences will be outlined in this guide. +This page provides steps to use Testrun within a virtual machine in VirtualBox. VMWare and other providers haven't been tested yet. You should use these instructions alongside the [Get started guide](/docs/get_started.md). -## Prerequisites +# Prerequisites -### Hardware +## Hardware -Before starting with Testrun, ensure you have the following hardware: -- PC running any OS that supports Virtual Box -- 2x USB Ethernet adapter (built in ethernet connections are not supported) -- Internet connection +Before you start with Testrun, ensure you have the following hardware: -### Software +- PC running any OS that supports VirtualBox +- 2x USB Ethernet adapter (built-in Ethernet connections aren't supported) +- Internet connection -Ensure the following software is installed on the host PC: - - Virtual Box +## Software -Ensure the following software is installed on your virtual machine: -- Ubuntu LTS (22.04 or 20.04) -- Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) +Ensure you have VirtualBox installed on the host PC. Then, install the following software on your virtual machine: -## Installation +- Ubuntu LTS (22.04 or 24.04) +- Docker + - Refer to the [installation guide](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) as needed. -In addition to the install steps provided in the Get Started guide, the default user must be added to the sudo group. -1. Open a terminal and run ```sudo su``` to login as root (you will be prompted for your password). -2. Add the default user to the sudo group by running ```adduser {username} sudo```. -3. Restart the virtual machine. -4. Continue the installation as per the Get Started guide. +# Installation -## Start Testrun +As part of installation, you must add the default user to the sudo group: + +1. Open a terminal and run `sudo su` to log in as root. +1. Enter your password when prompted. +1. Add the default user to the sudo group by running `adduser {username} sudo`. +1. Restart the virtual machine. +1. Follow the steps in the [Get started guide](/docs/get_started.md) to complete the installation. + +# Start Testrun + +Follow these steps to start Testrun. Keep in mind that attaching USB Ethernet adapters is different when working in a virtual machine. -Attaching USB ethernet adapters is different when working in a Virtual Machine. 1. Ensure the 2x adapters are attached to the host PC. -2. With the virtual machine running, right click the USB icon in the bottom right of the window. -3. Select the 2x ethernet adapter names and check that these two adapters have now appeared in the virtual machine. \ No newline at end of file +1. With the virtual machine running, right-click the **USB** icon in the bottom-right of the window. +1. Select the 2x Ethernet adapter names. The two adapters should now appear in the virtual machine. \ No newline at end of file diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index aed663ab8..a11b35160 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -24,10 +24,11 @@ import signal import threading import uvicorn -from urllib.parse import urlparse +from core import tasks from common import logger from common.device import Device +from common.statuses import TestrunStatus LOGGER = logger.get_logger("api") @@ -35,8 +36,17 @@ DEVICE_MANUFACTURER_KEY = "manufacturer" DEVICE_MODEL_KEY = "model" DEVICE_TEST_MODULES_KEY = "test_modules" +DEVICE_TEST_PACK_KEY = "test_pack" +DEVICE_TYPE_KEY = "type" +DEVICE_TECH_KEY = "technology" +DEVICE_ADDITIONAL_INFO_KEY = "additional_info" + DEVICES_PATH = "local/devices" -DEFAULT_DEVICE_INTF = "enx123456789123" +PROFILES_PATH = "local/risk_profiles" + +RESOURCES_PATH = "resources" +DEVICE_FOLDER_PATH = "devices" +DEVICE_QUESTIONS_FILE_NAME = "device_profile.json" LATEST_RELEASE_CHECK = ("https://api.github.com/repos/google/" + "testrun/releases/latest") @@ -45,32 +55,45 @@ class Api: """Provide REST endpoints to manage Testrun""" - def __init__(self, test_run): + def __init__(self, testrun): - self._test_run = test_run + self._testrun = testrun self._name = "Testrun API" self._router = APIRouter() - self._session = self._test_run.get_session() + # Load static JSON resources + device_resources = os.path.join(self._testrun.get_root_dir(), + RESOURCES_PATH, + DEVICE_FOLDER_PATH) + + # Load device profile questions + self._device_profile = self._load_json(device_resources, + DEVICE_QUESTIONS_FILE_NAME) + + # Fetch Testrun session + self._session = self._testrun.get_session() + # System endpoints self._router.add_api_route("/system/interfaces", self.get_sys_interfaces) self._router.add_api_route("/system/config", self.post_sys_config, methods=["POST"]) self._router.add_api_route("/system/config", self.get_sys_config) self._router.add_api_route("/system/start", - self.start_test_run, + self.start_testrun, methods=["POST"]) self._router.add_api_route("/system/stop", - self.stop_test_run, + self.stop_testrun, methods=["POST"]) self._router.add_api_route("/system/status", self.get_status) self._router.add_api_route("/system/shutdown", self.shutdown, methods=["POST"]) - self._router.add_api_route("/system/version", self.get_version) + self._router.add_api_route("/system/modules", self.get_test_modules) + self._router.add_api_route("/system/testpacks", self.get_test_packs) + # Report endpoints self._router.add_api_route("/reports", self.get_reports) self._router.add_api_route("/report", self.delete_report, @@ -81,6 +104,7 @@ def __init__(self, test_run): self.get_results, methods=["POST"]) + # Device endpoints self._router.add_api_route("/devices", self.get_devices) self._router.add_api_route("/device", self.delete_device, @@ -89,10 +113,9 @@ def __init__(self, test_run): self._router.add_api_route("/device/edit", self.edit_device, methods=["POST"]) + self._router.add_api_route("/devices/format", self.get_devices_profile) - # Load modules - self._router.add_api_route("/system/modules", self.get_test_modules) - + # Certificate endpoints self._router.add_api_route("/system/config/certs", self.get_certs) self._router.add_api_route("/system/config/certs", self.upload_cert, @@ -110,12 +133,23 @@ def __init__(self, test_run): self._router.add_api_route("/profiles", self.delete_profile, methods=["DELETE"]) + self._router.add_api_route("/profile/{profile_name}", + self.export_profile, + methods=["POST"]) # Allow all origins to access the API origins = ["*"] - self._app = FastAPI() + # Scheduler for background periodic tasks + self._scheduler = tasks.PeriodicTasks(self._testrun) + + # Init FastAPI + self._app = FastAPI(lifespan=self._scheduler.start) + + # Attach router to FastAPI self._app.include_router(self._router) + + # Attach CORS middleware self._app.add_middleware( CORSMiddleware, allow_origins=origins, @@ -124,10 +158,27 @@ def __init__(self, test_run): allow_headers=["*"], ) + # Use separate thread for API self._api_thread = threading.Thread(target=self._start, name="Testrun API", daemon=True) + def _load_json(self, directory, file_name): + """Utility method to load json files' """ + # Construct the base path relative to the main folder + root_dir = self._testrun.get_root_dir() + + # Construct the full file path + file_path = os.path.join(root_dir, directory, file_name) + + # Open the file in read mode + with open(file_path, "r", encoding="utf-8") as file: + # Return the file content + return json.load(file) + + def _get_testrun(self): + return self._testrun + def start(self): LOGGER.info("Starting API") self._api_thread.start() @@ -165,7 +216,19 @@ async def post_sys_config(self, request: Request, response: Response): try: config = (await request.body()).decode("UTF-8") config_json = json.loads(config) + + # Validate req fields + if ("network" not in config_json or + "device_intf" not in config_json.get("network") or + "internet_intf" not in config_json.get("network") or + "log_level" not in config_json): + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg( + False, + "Configuration is missing required fields") + self._session.set_config(config_json) + # Catch JSON Decode error etc except JSONDecodeError: response.status_code = status.HTTP_400_BAD_REQUEST @@ -176,9 +239,12 @@ async def get_sys_config(self): return self._session.get_config() async def get_devices(self): - return self._session.get_device_repository() + devices = [] + for device in self._session.get_device_repository(): + devices.append(device.to_dict()) + return devices - async def start_test_run(self, request: Request, response: Response): + async def start_testrun(self, request: Request, response: Response): LOGGER.debug("Received start command") @@ -199,9 +265,25 @@ async def start_test_run(self, request: Request, response: Response): device = self._session.get_device(body_json["device"]["mac_addr"]) + # Check if requested device is known in the device repository + if device is None: + response.status_code = status.HTTP_404_NOT_FOUND + return self._generate_msg( + False, "A device with that MAC address could not be found") + + # Check if device is fully configured + if device.status != "Valid": + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg(False, "Device configuration is not complete") + # Check Testrun is not already running - if self._test_run.get_session().get_status() in [ - "In Progress", "Waiting for Device", "Monitoring" + if self._testrun.get_session().get_status() in [ + TestrunStatus.IN_PROGRESS, + TestrunStatus.WAITING_FOR_DEVICE, + TestrunStatus.MONITORING, + TestrunStatus.VALIDATING, + TestrunStatus.STARTING + ]: LOGGER.debug("Testrun is already running. Cannot start another instance") response.status_code = status.HTTP_409_CONFLICT @@ -209,40 +291,42 @@ async def start_test_run(self, request: Request, response: Response): False, "Testrun cannot be started " + "whilst a test is running on another device") - # Check if requested device is known in the device repository - if device is None: - response.status_code = status.HTTP_404_NOT_FOUND - return self._generate_msg( - False, "A device with that MAC address could not be found") - device.firmware = body_json["device"]["firmware"] # Check if config has been updated (device interface not default) - if (self._test_run.get_session().get_device_interface() == - DEFAULT_DEVICE_INTF): + if (self._testrun.get_session().get_device_interface() == + ""): response.status_code = status.HTTP_400_BAD_REQUEST return self._generate_msg( False, "Testrun configuration has not yet " + "been completed.") # Check Testrun is able to start - if self._test_run.get_net_orc().check_config() is False: + if self._testrun.get_net_orc().check_config() is False: response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return self._generate_msg( False, "Configured interfaces are not " + "ready for use. Ensure required interfaces " + "are connected.") - device.test_modules = body_json["device"]["test_modules"] + # UI doesn't send individual test configs so we need to + # merge these manually until the UI is updated to handle + # the full config file + for module_name, module_config in device.test_modules.items(): + # Check if the module exists in UI test modules + if module_name in body_json["device"]["test_modules"]: + # Merge the enabled state + module_config["enabled"] = body_json[ + "device"]["test_modules"][module_name]["enabled"] LOGGER.info("Starting Testrun with device target " + f"{device.manufacturer} {device.model} with " + f"MAC address {device.mac_addr}") - thread = threading.Thread(target=self._start_test_run, name="Testrun") + thread = threading.Thread(target=self._start_testrun, name="Testrun") thread.start() - self._test_run.get_session().set_target_device(device) + self._testrun.get_session().set_target_device(device) - return self._test_run.get_session().to_json() + return self._testrun.get_session().to_json() def _generate_msg(self, success, message): msg_type = "success" @@ -250,24 +334,28 @@ def _generate_msg(self, success, message): msg_type = "error" return json.loads('{"' + msg_type + '": "' + message + '"}') - def _start_test_run(self): - self._test_run.start() + def _start_testrun(self): + self._testrun.start() - async def stop_test_run(self, response: Response): + async def stop_testrun(self, response: Response): LOGGER.debug("Received stop command") # Check if Testrun is running - if (self._test_run.get_session().get_status() - not in ["In Progress", "Waiting for Device", "Monitoring"]): + if (self._testrun.get_session().get_status() + not in [TestrunStatus.IN_PROGRESS, + TestrunStatus.WAITING_FOR_DEVICE, + TestrunStatus.MONITORING, + TestrunStatus.VALIDATING, + TestrunStatus.STARTING]): response.status_code = 404 return self._generate_msg(False, "Testrun is not currently running") - self._test_run.stop() + self._testrun.stop() return self._generate_msg(True, "Testrun stopped") async def get_status(self): - return self._test_run.get_session().to_json() + return self._testrun.get_session().to_json() def shutdown(self, response: Response): @@ -275,20 +363,25 @@ def shutdown(self, response: Response): # Check that Testrun is not currently running if (self._session.get_status() - not in ["Cancelled", "Compliant", "Non-Compliant", "Idle"]): + not in [TestrunStatus.CANCELLED, + TestrunStatus.PROCEED, + TestrunStatus.DO_NOT_PROCEED, + TestrunStatus.COMPLETE, + TestrunStatus.IDLE + ]): LOGGER.debug("Unable to shutdown Testrun as Testrun is in progress") response.status_code = 400 return self._generate_msg( False, "Unable to shutdown. A test is currently in progress.") - self._test_run.shutdown() + self._testrun.shutdown() os.kill(os.getpid(), signal.SIGTERM) async def get_version(self, response: Response): # Add defaults json_response = {} - json_response["installed_version"] = "v" + self._test_run.get_version() + json_response["installed_version"] = "v" + self._testrun.get_version() json_response["update_available"] = False json_response["latest_version"] = None json_response["latest_version_url"] = ( @@ -343,15 +436,13 @@ async def get_reports(self, request: Request): LOGGER.debug("Received reports list request") # Resolve the server IP from the request so we # can fix the report URL - client_origin = request.headers.get("Origin") - parsed_url = urlparse(client_origin) - server_ip = parsed_url.hostname # This will give you the IP address - reports = self._session.get_all_reports() for report in reports: # report URL is currently hard coded as localhost so we can # replace that to fix the IP dynamically from the requester - report["report"] = report["report"].replace("localhost", server_ip) + report["report"] = report["report"].replace( + "localhost", request.client.host) + report["export"] = report["report"].replace("report", "export") return reports async def delete_report(self, request: Request, response: Response): @@ -360,7 +451,7 @@ async def delete_report(self, request: Request, response: Response): if len(body_raw) == 0: response.status_code = 400 - return self._generate_msg(False, "Invalid request received") + return self._generate_msg(False, "Invalid request received, missing body") try: body_json = json.loads(body_raw) @@ -372,12 +463,18 @@ async def delete_report(self, request: Request, response: Response): if "mac_addr" not in body_json or "timestamp" not in body_json: response.status_code = 400 - return self._generate_msg(False, "Invalid request received") + return self._generate_msg(False, "Missing mac address or timestamp") mac_addr = body_json.get("mac_addr").lower() timestamp = body_json.get("timestamp") - parsed_timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") - timestamp_formatted = parsed_timestamp.strftime("%Y-%m-%dT%H:%M:%S") + + try: + parsed_timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") + timestamp_formatted = parsed_timestamp.strftime("%Y-%m-%dT%H:%M:%S") + + except ValueError: + response.status_code = 400 + return self._generate_msg(False, "Incorrect timestamp format") # Get device from MAC address device = self._session.get_device(mac_addr) @@ -386,11 +483,19 @@ async def delete_report(self, request: Request, response: Response): response.status_code = 404 return self._generate_msg(False, "Could not find device") - if self._test_run.delete_report(device, timestamp_formatted): + # Assign the reports folder path from testrun + reports_folder = self._testrun.get_reports_folder(device) + + # Check if reports folder exists + if not os.path.exists(reports_folder): + response.status_code = 404 + return self._generate_msg(False, "Report not found") + + if self._testrun.delete_report(device, timestamp_formatted): return self._generate_msg(True, "Deleted report") response.status_code = 500 - return self._generate_msg(False, "Error occured whilst deleting report") + return self._generate_msg(False, "Error occurred whilst deleting report") async def delete_device(self, request: Request, response: Response): @@ -410,7 +515,7 @@ async def delete_device(self, request: Request, response: Response): mac_addr = device_json.get("mac_addr").lower() # Check that device exists - device = self._test_run.get_session().get_device(mac_addr) + device = self._testrun.get_session().get_device(mac_addr) if device is None: response.status_code = 404 @@ -419,13 +524,18 @@ async def delete_device(self, request: Request, response: Response): # Check that Testrun is not currently running against this device if (self._session.get_target_device() == device and self._session.get_status() - not in ["Cancelled", "Compliant", "Non-Compliant"]): + not in [TestrunStatus.CANCELLED, + TestrunStatus.COMPLETE, + TestrunStatus.PROCEED, + TestrunStatus.DO_NOT_PROCEED + ]): + response.status_code = 403 return self._generate_msg( - False, "Cannot delete this device whilst " + "it is being tested") + False, "Cannot delete this device whilst it is being tested") # Delete device - self._test_run.delete_device(device) + self._testrun.delete_device(device) # Return success response response.status_code = 200 @@ -436,7 +546,7 @@ async def delete_device(self, request: Request, response: Response): LOGGER.error(e) response.status_code = 500 return self._generate_msg( - False, "An error occured whilst deleting " + "the device") + False, "An error occurred whilst deleting the device") async def save_device(self, request: Request, response: Response): LOGGER.debug("Received device post request") @@ -464,6 +574,19 @@ async def save_device(self, request: Request, response: Response): device_json.get(DEVICE_MODEL_KEY) ) + # Check if device folder exists + device_folder = os.path.join(self._testrun.get_root_dir(), + DEVICES_PATH, + device_json.get(DEVICE_MANUFACTURER_KEY) + + " " + + device_json.get(DEVICE_MODEL_KEY)) + + if os.path.exists(device_folder): + response.status_code = status.HTTP_409_CONFLICT + return self._generate_msg( + False, "A folder with that name already exists, " \ + "please rename the device or folder") + if device is None: # Create new device @@ -471,10 +594,15 @@ async def save_device(self, request: Request, response: Response): device.mac_addr = device_json.get(DEVICE_MAC_ADDR_KEY).lower() device.manufacturer = device_json.get(DEVICE_MANUFACTURER_KEY) device.model = device_json.get(DEVICE_MODEL_KEY) + device.test_pack = device_json.get(DEVICE_TEST_PACK_KEY) + device.type = device_json.get(DEVICE_TYPE_KEY) + device.technology = device_json.get(DEVICE_TECH_KEY) + device.additional_info = device_json.get(DEVICE_ADDITIONAL_INFO_KEY) + device.device_folder = device.manufacturer + " " + device.model device.test_modules = device_json.get(DEVICE_TEST_MODULES_KEY) - self._test_run.create_device(device) + self._testrun.create_device(device) response.status_code = status.HTTP_201_CREATED else: @@ -517,14 +645,18 @@ async def edit_device(self, request: Request, response: Response): if device is None: response.status_code = status.HTTP_404_NOT_FOUND return self._generate_msg( - False, "A device with that MAC " + "address could not be found") + False, "A device with that MAC address could not be found") if (self._session.get_target_device() == device and self._session.get_status() - not in ["Cancelled", "Compliant", "Non-Compliant"]): + not in [TestrunStatus.CANCELLED, + TestrunStatus.COMPLETE, + TestrunStatus.PROCEED, + TestrunStatus.DO_NOT_PROCEED + ]): response.status_code = 403 return self._generate_msg( - False, "Cannot edit this device whilst " + "it is being tested") + False, "Cannot edit this device whilst it is being tested") # Check if a device exists with the new MAC address check_new_device = self._session.get_device( @@ -534,15 +666,22 @@ async def edit_device(self, request: Request, response: Response): != check_new_device.mac_addr): response.status_code = status.HTTP_409_CONFLICT return self._generate_msg( - False, "A device with that MAC address " + "already exists") + False, "A device with that MAC address already exists") # Update the device device.mac_addr = device_json.get(DEVICE_MAC_ADDR_KEY).lower() device.manufacturer = device_json.get(DEVICE_MANUFACTURER_KEY) device.model = device_json.get(DEVICE_MODEL_KEY) + device.test_pack = device_json.get(DEVICE_TEST_PACK_KEY) + device.type = device_json.get(DEVICE_TYPE_KEY) + device.technology = device_json.get(DEVICE_TECH_KEY) + device.additional_info = device_json.get(DEVICE_ADDITIONAL_INFO_KEY) device.test_modules = device_json.get(DEVICE_TEST_MODULES_KEY) - self._test_run.save_device(device, device_json) + # Update device status to valid now that configuration is complete + device.status = "Valid" + + self._testrun.save_device(device) response.status_code = status.HTTP_200_OK return device.to_config_json() @@ -555,6 +694,12 @@ async def edit_device(self, request: Request, response: Response): async def get_report(self, response: Response, device_name, timestamp): device = self._session.get_device_by_name(device_name) + # If the device not found + if device is None: + LOGGER.info("Device not found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Device not found") + # 1.3 file path file_path = os.path.join( DEVICES_PATH, @@ -608,47 +753,77 @@ async def get_results(self, request: Request, response: Response, device_name, return self._generate_msg(False, "A device with that name could not be found") - file_path = self._get_test_run().get_test_orc().zip_results( + # Check if report exists (1.3 file path) + report_file_path = os.path.join( + DEVICES_PATH, + device_name, + "reports", + timestamp,"test", + device.mac_addr.replace(":","")) + + if not os.path.isdir(report_file_path): + # pre 1.3 file path + report_file_path = os.path.join(DEVICES_PATH, device_name, "reports", + timestamp) + + if not os.path.isdir(report_file_path): + LOGGER.info("Report could not be found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Report could not be found") + + zip_file_path = self._get_testrun().get_test_orc().zip_results( device, timestamp, profile) - if file_path is None: + if zip_file_path is None: response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return self._generate_msg( False, "An error occurred whilst archiving test results") - if os.path.isfile(file_path): - return FileResponse(file_path) + if os.path.isfile(zip_file_path): + return FileResponse(zip_file_path) else: LOGGER.info("Test results could not be found, returning 404") response.status_code = 404 return self._generate_msg(False, "Test results could not be found") + async def get_devices_profile(self): + """Device profile questions""" + return self._device_profile + def _validate_device_json(self, json_obj): # Check all required properties are present - if not (DEVICE_MAC_ADDR_KEY in json_obj and DEVICE_MANUFACTURER_KEY - in json_obj and DEVICE_MODEL_KEY in json_obj): - return False + for string in [ + DEVICE_MAC_ADDR_KEY, + DEVICE_MANUFACTURER_KEY, + DEVICE_MODEL_KEY, + DEVICE_TYPE_KEY, + DEVICE_TECH_KEY, + DEVICE_ADDITIONAL_INFO_KEY + ]: + if string not in json_obj: + LOGGER.error(f"Missing required key {string} in device configuration") + return False # Check length of strings if len(json_obj.get(DEVICE_MANUFACTURER_KEY)) > 28 or len( json_obj.get(DEVICE_MODEL_KEY)) > 28: + LOGGER.error("Device manufacturer or model are longer than 28 characters") return False disallowed_chars = ["/", "\\", "\'", "\"", ";"] for char in json_obj.get(DEVICE_MANUFACTURER_KEY): if char in disallowed_chars: + LOGGER.error("Disallowed character in device manufacturer") return False for char in json_obj.get(DEVICE_MODEL_KEY): if char in disallowed_chars: + LOGGER.error("Disallowed character in device model") return False return True - def _get_test_run(self): - return self._test_run - # Profiles def get_profiles_format(self, response: Response): @@ -670,6 +845,12 @@ async def update_profile(self, request: Request, response: Response): LOGGER.debug("Received profile update request") + # Check if the profiles format was loaded correctly + if self.get_session().get_profiles_format() is None: + response.status_code = status.HTTP_501_NOT_IMPLEMENTED + return self._generate_msg(False, + "Risk profiles are not available right now") + try: req_raw = (await request.body()).decode("UTF-8") req_json = json.loads(req_raw) @@ -679,12 +860,18 @@ async def update_profile(self, request: Request, response: Response): response.status_code = status.HTTP_400_BAD_REQUEST return self._generate_msg(False, "Invalid request received") + # Validate json profile + if not self.get_session().validate_profile_json(req_json): + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg(False, "Invalid request received") + profile_name = req_json.get("name") # Check if profile exists profile = self.get_session().get_profile(profile_name) if profile is None: + # Create new profile profile = self.get_session().update_profile(req_json) @@ -746,6 +933,93 @@ async def delete_profile(self, request: Request, response: Response): return self._generate_msg(True, "Successfully deleted that profile") + async def export_profile(self, request: Request, response: Response, + profile_name): + + LOGGER.debug(f"Received get profile request for {profile_name}") + + device = None + + try: + req_raw = (await request.body()).decode("UTF-8") + req_json = json.loads(req_raw) + + # Check if device mac_addr has been specified + if "mac_addr" in req_json and len(req_json.get("mac_addr")) > 0: + device_mac_addr = req_json.get("mac_addr") + device = self.get_session().get_device(device_mac_addr) + + # If device is not found return 404 + if device is None: + response.status_code = status.HTTP_404_NOT_FOUND + return self._generate_msg( + False, "A device with that mac address could not be found") + + except JSONDecodeError: + # Device not specified + pass + + # Retrieve the profile + profile = self._session.get_profile(profile_name) + + # If the profile not found return 404 + if profile is None: + LOGGER.info("Profile not found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Profile could not be found") + + # If device has been added into the body + if device: + + try: + + # Path where the PDF will be saved + profile_pdf_path = os.path.join(PROFILES_PATH, f"{profile_name}.pdf") + + # Write the PDF content + with open(profile_pdf_path, "wb") as f: + f.write(profile.to_pdf(device).getvalue()) + + # Return the pdf file + if os.path.isfile(profile_pdf_path): + return FileResponse(profile_pdf_path) + else: + LOGGER.info("Profile could not be found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Profile could not be found") + + # Exceptions if the PDF creation fails + except Exception as e: + LOGGER.error(f"Error creating the profile PDF: {e}") + response.status_code = 500 + return self._generate_msg(False, "Error retrieving the profile PDF") + + # If device not added into the body + else: + + try: + + # Path where the PDF will be saved + profile_pdf_path = os.path.join(PROFILES_PATH, f"{profile_name}.pdf") + + # Write the PDF content + with open(profile_pdf_path, "wb") as f: + f.write(profile.to_pdf_no_device().getvalue()) + + # Return the pdf file + if os.path.isfile(profile_pdf_path): + return FileResponse(profile_pdf_path) + else: + LOGGER.info("Profile could not be found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Profile could not be found") + + # Exceptions if the PDF creation fails + except Exception as e: + LOGGER.error(f"Error creating the profile PDF: {e}") + response.status_code = 500 + return self._generate_msg(False, "Error retrieving the profile PDF") + # Certificates def get_certs(self): LOGGER.debug("Received certs list request") @@ -798,6 +1072,20 @@ async def upload_cert(self, file: UploadFile, response: Response): False, "A certificate with that common name already exists." ) + # Returned when organization name is missing + elif str(e) == "Certificate is missing the organization name": + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg( + False, "The certificate must contain the organization name" + ) + + # Returned when common name is missing + elif str(e) == "Certificate is missing the common name": + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg( + False, "The certificate must contain the common name" + ) + # Returned when unable to load PEM file else: response.status_code = status.HTTP_400_BAD_REQUEST @@ -845,7 +1133,13 @@ async def delete_cert(self, request: Request, response: Response): def get_test_modules(self): modules = [] - for module in self._test_run.get_test_orc().get_test_modules(): + for module in self._testrun.get_test_orc().get_test_modules(): if module.enabled and module.enable_container: modules.append(module.display_name) return modules + + def get_test_packs(self): + test_packs: list[str] = [] + for test_pack in self._testrun.get_test_orc().get_test_packs(): + test_packs.append(test_pack.name) + return test_packs diff --git a/framework/python/src/common/device.py b/framework/python/src/common/device.py index c6a289d2c..d90720d90 100644 --- a/framework/python/src/common/device.py +++ b/framework/python/src/common/device.py @@ -14,7 +14,7 @@ """Track device object information.""" -from typing import Dict, List +from typing import List, Dict from dataclasses import dataclass, field from common.testreport import TestReport from datetime import datetime @@ -23,17 +23,21 @@ class Device(): """Represents a physical device and it's configuration.""" + status: str = 'Valid' folder_url: str = None mac_addr: str = None manufacturer: str = None model: str = None + type: str = None + technology: str = None + test_pack: str = 'Device Qualification' + additional_info: List[dict] = field(default_factory=list) test_modules: Dict = field(default_factory=dict) ip_addr: str = None firmware: str = None device_folder: str = None reports: List[TestReport] = field(default_factory=list) max_device_reports: int = None - reports: List[TestReport] = field(default_factory=list) def add_report(self, report): self.reports.append(report) @@ -54,11 +58,18 @@ def to_dict(self): """Returns the device as a python dictionary. This is used for the system status API endpoint and in the report.""" device_json = {} + device_json['status'] = self.status device_json['mac_addr'] = self.mac_addr device_json['manufacturer'] = self.manufacturer device_json['model'] = self.model + device_json['type'] = self.type + device_json['technology'] = self.technology + device_json['test_pack'] = self.test_pack + device_json['additional_info'] = self.additional_info + if self.firmware is not None: device_json['firmware'] = self.firmware + device_json['test_modules'] = self.test_modules return device_json @@ -69,5 +80,9 @@ def to_config_json(self): device_json['mac_addr'] = self.mac_addr device_json['manufacturer'] = self.manufacturer device_json['model'] = self.model + device_json['type'] = self.type + device_json['technology'] = self.technology + device_json['test_pack'] = self.test_pack device_json['test_modules'] = self.test_modules + device_json['additional_info'] = self.additional_info return device_json diff --git a/framework/python/src/common/docker_util.py b/framework/python/src/common/docker_util.py new file mode 100644 index 000000000..06b030419 --- /dev/null +++ b/framework/python/src/common/docker_util.py @@ -0,0 +1,35 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Utility for common docker methods""" +import docker + +def create_private_net(network_name): + client = docker.from_env() + try: + network = client.networks.get(network_name) + network.remove() + except docker.errors.NotFound: + pass + + # TODO: These should be made into variables + ipam_pool = docker.types.IPAMPool(subnet='100.100.0.0/16', + iprange='100.100.100.0/24') + + ipam_config = docker.types.IPAMConfig(pool_configs=[ipam_pool]) + + client.networks.create(network_name, + ipam=ipam_config, + internal=True, + check_duplicate=True, + driver='macvlan') diff --git a/framework/python/src/common/mqtt.py b/framework/python/src/common/mqtt.py new file mode 100644 index 000000000..b98b4ab1b --- /dev/null +++ b/framework/python/src/common/mqtt.py @@ -0,0 +1,60 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""MQTT client""" +import json +import typing as t +import paho.mqtt.client as mqtt_client +from common import logger + +LOGGER = logger.get_logger("mqtt") +WEBSOCKETS_HOST = "localhost" +WEBSOCKETS_PORT = 1883 + +class MQTTException(Exception): + def __init__(self, message: str) -> None: + super().__init__(message) + + +class MQTT: + """ MQTT client class""" + def __init__(self) -> None: + self._host = WEBSOCKETS_HOST + self._client = mqtt_client.Client(mqtt_client.CallbackAPIVersion.VERSION2) + + def _connect(self): + """Establish connection to MQTT broker""" + if not self._client.is_connected(): + try: + self._client.connect(self._host, WEBSOCKETS_PORT, 60) + except (ValueError, ConnectionRefusedError): + LOGGER.error("Cannot connect to MQTT broker") + + def disconnect(self): + """Disconnect the local client from the MQTT broker""" + if self._client.is_connected(): + LOGGER.debug("Disconnecting from broker") + self._client.disconnect() + + def send_message(self, topic: str, message: t.Union[str, dict]) -> None: + """Send message to specific topic + + Args: + topic (str): mqtt topic + message (t.Union[str, dict]): message + """ + self._connect() + if isinstance(message, dict): + message = json.dumps(message) + self._client.publish(topic, str(message)) diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py index 6afb229ac..f2e684710 100644 --- a/framework/python/src/common/risk_profile.py +++ b/framework/python/src/common/risk_profile.py @@ -20,10 +20,16 @@ from common import logger import json import os +from jinja2 import Template +from copy import deepcopy +import math PROFILES_PATH = 'local/risk_profiles' LOGGER = logger.get_logger('risk_profile') RESOURCES_DIR = 'resources/report' +TEMPLATE_FILE = 'risk_report_template.html' +TEMPLATE_STYLES = 'risk_report_styles.css' +DEVICE_FORMAT_PATH = 'resources/devices/device_profile.json' # Locate parent directory current_dir = os.path.dirname(os.path.realpath(__file__)) @@ -43,6 +49,33 @@ class RiskProfile(): def __init__(self, profile_json=None, profile_format=None): + # Jinja template + with open(os.path.join(report_resource_dir, TEMPLATE_FILE), + 'r', + encoding='UTF-8' + ) as template_file: + self._template = Template(template_file.read()) + with open(os.path.join(report_resource_dir, + TEMPLATE_STYLES), + 'r', + encoding='UTF-8' + ) as style_file: + self._template_styles = style_file.read() + + # Device profile format + self._device_format = [] + try: + with open(os.path.join(root_dir, DEVICE_FORMAT_PATH), + 'r', + encoding='utf-8') as device_format_file: + device_format_json = json.load(device_format_file) + self._device_format = device_format_json + except (IOError, ValueError) as e: + LOGGER.error( + 'An error occurred whilst loading the device profile format') + LOGGER.debug(e) + + if profile_json is None or profile_format is None: return @@ -92,15 +125,16 @@ def update(self, profile_json, profile_format): self.risk = new_profile.risk def get_file_path(self): + """Returns the file path for the current risk profile json""" return os.path.join(PROFILES_PATH, self.name + '.json') def _validate(self, profile_json, profile_format): - if self._valid(profile_json, profile_format): - if self._expired(): - self.status = 'Expired' + if self._expired(): + self.status = 'Expired' + elif self._valid(profile_json, profile_format): # User only wants to save a draft - elif 'status' in profile_json and profile_json['status'] == 'Draft': + if 'status' in profile_json and profile_json['status'] == 'Draft': self.status = 'Draft' else: self.status = 'Valid' @@ -108,6 +142,7 @@ def _validate(self, profile_json, profile_format): self.status = 'Draft' def update_risk(self, profile_format): + """Update the calculated risk for the risk profile""" if self.status == 'Valid': @@ -176,6 +211,15 @@ def update_risk(self, profile_format): self.risk = risk + def _update_risk_by_device(self): + risk = self.risk + if self._device and self.status == 'Valid': + for question in self._device.additional_info: + if 'risk' in question and question['risk'] == 'High': + risk = 'High' + break + return risk + def _get_format_question(self, question: str, profile_format: dict): for q in profile_format: @@ -281,6 +325,7 @@ def _expired(self): return today > expiry_date def to_json(self, pretty=False): + """Returns the current risk profile in JSON format""" json_dict = { 'name': self.name, 'version': self.version, @@ -293,356 +338,177 @@ def to_json(self, pretty=False): return json.dumps(json_dict, indent=indent) def to_html(self, device): + """Returns the current risk profile in HTML format""" + + high_risk_message = '''The device has been assessed to be high + risk due to the nature of the answers provided + about the device functionality.''' + limited_risk_message = '''The device has been assessed to be limited risk + due to the nature of the answers provided about + the device functionality.''' + with open(test_run_img_file, 'rb') as f: + logo_img_b64 = base64.b64encode(f.read()).decode('utf-8') + + self._device = self._format_device_profile(device) + pages = self._generate_report_pages(device) + return self._template.render( + styles=self._template_styles, + manufacturer=self._device.manufacturer, + model=self._device.model, + logo=logo_img_b64, + risk=self._update_risk_by_device(), + high_risk_message=high_risk_message, + limited_risk_message=limited_risk_message, + pages=pages, + total_pages=len(pages), + version=self.version, + created_at=self.created.strftime('%d.%m.%Y') + ) + + def to_html_no_device(self): + """Returns the risk profile in HTML format without device info""" + + high_risk_message = '''The device has been assessed to be high + risk due to the nature of the answers provided + about the device functionality.''' + limited_risk_message = '''The device has been assessed to be limited risk + due to the nature of the answers provided about + the device functionality.''' - self._device = device - - return f''' - - - {self._generate_head()} - -
- {self._generate_header()} - {self._generate_risk_banner()} - {self._generate_risk_questions()} - {self._generate_footer()} -
- - - ''' - - def _generate_head(self): - - return f''' - - - - Risk Assessment - - - ''' - - def _generate_header(self): with open(test_run_img_file, 'rb') as f: - tr_img_b64 = base64.b64encode(f.read()).decode('utf-8') - header = f''' -
-

Risk assessment

-

- {self._device.manufacturer} - {self._device.model} -

''' - header += f'''Testrun -
- ''' - return header - - def _generate_risk_banner(self): - return f''' -
-
-

{'high' if self.risk == 'High' else 'limited'} Risk

-
-
- { - 'The device has been assessed to be high risk due to the nature of the answers provided about the device functionality.' - if self.risk == 'High' else - 'The device has been assessed to be limited risk due to the nature of the answers provided about the device functionality.' - } -
-
- ''' - - def _generate_risk_questions(self): - - max_page_height = 350 - content = '' - - content += self._generate_table_head() + logo_img_b64 = base64.b64encode(f.read()).decode('utf-8') + + pages = self._generate_report_pages() + return self._template.render( + styles=self._template_styles, + logo=logo_img_b64, + risk=self.risk, + high_risk_message=high_risk_message, + limited_risk_message=limited_risk_message, + pages=pages, + total_pages=len(pages), + version=self.version, + created_at=self.created.strftime('%d.%m.%Y') + ) + + def _generate_report_pages(self, device=None): + + # Text block heght + block_height = 18 + # Table row padding + block_padding = 30 + # Margin bottom list answer + margin_list = 14 + # margin after table row + margin_row = 8 + + height_first_page = 760 + height_page = 980 + + # Average text block width in characters + # for a 14px font size (average width of one character is 8px). + letters_in_line_str = 38 + letters_in_line_q = 40 + letters_in_line_list = 36 - index = 1 height = 0 + pages = [] + current_page = [] + index = 1 - for question in self.questions: + questions = deepcopy(self.questions) - if height > max_page_height: - content += self._generate_new_page() - height = 0 + if device: + questions = deepcopy(self._device.additional_info) + questions.extend(self.questions) + + for question in questions: - content += f''' -
-
{index}.
-
{question['question']}
-
''' - - # String answers (one line) - if isinstance(question['answer'], str): - content += question['answer'] - - if len(question['answer']) > 400: - height += 160 - elif len(question['answer']) > 300: - height += 140 - elif len(question['answer']) > 200: - height += 120 - elif len(question['answer']) > 100: - height += 70 - else: - height += 53 - - # Select multiple answers - elif isinstance(question['answer'], list): - content += '' - - content += '''
''' - + text_answers.append(options_dict[answer_index]['text']) + page_item['answer'] = text_answers + # Answer height for list + for answer in options: + answer_height += math.ceil(len(answer) + / letters_in_line_list + ) * block_height + answer_height += block_padding + margin_row + margin_list + page_item['index'] = index + row_height = max(question_height, answer_height) + + if ( + (len(pages) == 0 and row_height + height > height_first_page) + or (len(pages) > 0 and row_height + height > height_page) + ): + pages.append(current_page) + height = 0 + current_page = [page_item] + else: + height += row_height + current_page.append(page_item) index += 1 + pages.append(current_page) - return content - - def _generate_table_head(self): - return ''' -
-
-
Question
-
Answer
-
''' - - def _generate_new_page(self): - - # End the current table - content = ''' -
''' - - # End the page - content += self._generate_footer() - content += '' - - # Start a new page - content += ''' -
- ''' - - content += self._generate_header() - - content += self._generate_table_head() - - return content - - def _generate_footer(self): - footer = f''' - - ''' - return footer - - def _generate_css(self): - return ''' - /* Set some global variables */ - :root { - --header-height: .75in; - --header-width: 8.5in; - --header-pos-x: 0in; - --header-pos-y: 0in; - --page-width: 8.5in; - } - - @font-face { - font-family: 'Google Sans'; - font-style: normal; - src: url(https://fonts.gstatic.com/s/googlesans/v58/4Ua_rENHsxJlGDuGo1OIlJfC6l_24rlCK1Yo_Iqcsih3SAyH6cAwhX9RFD48TE63OOYKtrwEIJllpyk.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - - @font-face { - font-family: 'Roboto Mono'; - font-style: normal; - src: url(https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - - /* Define some common body formatting*/ - body { - font-family: 'Google Sans', sans-serif; - margin: 0px; - padding: 0px; - } - - /* Sets proper page size during print to pdf for weasyprint */ - @page { - size: Letter; - width: 8.5in; - height: 11in; - } - - .page { - position: relative; - margin: 0 20px; - width: 8.5in; - height: 11in; - } - - /* Define the header related css elements*/ - .header { - position: relative; - } - - h1 { - margin: 0 0 8px 0; - font-size: 20px; - font-weight: 400; - } - - h2 { - margin: 0px; - font-size: 48px; - font-weight: 700; - } - - h3 { - font-size: 24px; - margin-bottom: 10px; - margin-top: 15px; - } - - h4 { - font-size: 12px; - font-weight: 500; - color: #5F6368; - margin-bottom: 0; - margin-top: 0; - } - - /* CSS for the footer */ - .footer { - position: absolute; - height: 30px; - width: 8.5in; - bottom: 0in; - border-top: 1px solid #D3D3D3; - } - - .footer-label { - color: #3C4043; - position: absolute; - top: 5px; - font-size: 12px; - } - - @media print { - @page { - size: Letter; - width: 8.5in; - height: 11in; - } - } - - .risk-banner { - min-height: 120px; - padding: 5px 40px 0 40px; - margin-top: 30px; - } - - .risk-banner-limited { - background-color: #E4F7FB; - color: #007B83; - } - - .risk-banner-high { - background-color: #FCE8E6; - color: #C5221F; - } - - .risk-banner-title { - text-transform: uppercase; - font-weight: bold; - } - - .risk-table { - width: 100%; - margin-top: 40px; - text-align: left; - color: #3C4043; - font-size: 14px; - } + return pages - .risk-table-head { - margin-bottom: 15px; - } - - .risk-table-head-question { - display: inline-block; - margin-left: 70px; - font-weight: bold; - } - - .risk-table-head-answer { - display: inline-block; - margin-left: 325px; - font-weight: bold; - } - .risk-table-row { - margin-bottom: 8px; - background-color: #F8F9FA; - display: flex; - align-items: stretch; - overflow: hidden; - } - - .risk-question-no { - padding: 15px 20px; - width: 10px; - display: inline-block; - vertical-align: top; - position: relative; - } - - .risk-question { - padding: 15px 20px; - display: inline-block; - width: 350px; - vertical-align: top; - position: relative; - height: 100%; - } + def to_pdf(self, device): + """Returns the current risk profile in PDF format""" - .risk-answer { - background-color: #E8F0FE; - padding: 15px 20px; - display: inline-block; - width: 340px; - position: relative; - height: 100%; - } + # Resolve the data as html first + html = self.to_html(device) - ul { - margin-top: 0; - } - ''' + # Convert HTML to PDF in memory using weasyprint + pdf_bytes = BytesIO() + HTML(string=html).write_pdf(pdf_bytes) + return pdf_bytes - def to_pdf(self, device): + def to_pdf_no_device(self): + """Returns the risk profile in PDF format without device info""" # Resolve the data as html first - html = self.to_html(device) + html = self.to_html_no_device() # Convert HTML to PDF in memory using weasyprint pdf_bytes = BytesIO() HTML(string=html).write_pdf(pdf_bytes) return pdf_bytes + + # Adding risks to device profile questions + def _format_device_profile(self, device): + device_copy = deepcopy(device) + risk_map = { + question['question']: { + option['text']: option.get('risk', None) + for option in question['options'] if 'risk' in option + } + for question in self._device_format + } + for question in device_copy.additional_info: + risk = risk_map.get( + question['question'], {} + ).get(question['answer'], None) + if risk: + question['risk'] = risk + return device_copy diff --git a/framework/python/src/common/statuses.py b/framework/python/src/common/statuses.py new file mode 100644 index 000000000..33516390e --- /dev/null +++ b/framework/python/src/common/statuses.py @@ -0,0 +1,45 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Enums for Testrun""" + + +class TestrunStatus: + """Statuses for overall testing""" + IDLE = "Idle" + STARTING = "Starting" + WAITING_FOR_DEVICE = "Waiting for Device" + MONITORING = "Monitoring" + IN_PROGRESS = "In Progress" + CANCELLED = "Cancelled" + STOPPING = "Stopping" + VALIDATING = "Validating Network" + COMPLETE = "Complete" + PROCEED = "Proceed" + DO_NOT_PROCEED = "Do Not Proceed" + +class TestrunResult: + """Statuses for the Testrun result""" + COMPLIANT = "Compliant" + NON_COMPLIANT = "Non-Compliant" + +class TestResult: + """Statuses for test results""" + IN_PROGRESS = "In Progress" + COMPLIANT = "Compliant" + NON_COMPLIANT = "Non-Compliant" + ERROR = "Error" + FEATURE_NOT_DETECTED = "Feature Not Detected" + INFORMATIONAL = "Informational" + NOT_STARTED = "Not Started" + DISABLED = "Disabled" diff --git a/framework/python/src/common/tasks.py b/framework/python/src/common/tasks.py new file mode 100644 index 000000000..5da0b40c9 --- /dev/null +++ b/framework/python/src/common/tasks.py @@ -0,0 +1,78 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Periodic background tasks""" + +from contextlib import asynccontextmanager +import datetime +import logging + +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from fastapi import FastAPI + +from common import logger + +# Check adapters period seconds +# Check adapters period seconds +CHECK_NETWORK_ADAPTERS_PERIOD = 5 +CHECK_INTERNET_PERIOD = 2 +INTERNET_CONNECTION_TOPIC = 'events/internet' +NETWORK_ADAPTERS_TOPIC = 'events/adapter' + +LOGGER = logger.get_logger('tasks') + + +class PeriodicTasks: + """Background periodic tasks + """ + def __init__( + self, testrun_obj, + ) -> None: + self._testrun = testrun_obj + self._mqtt_client = self._testrun.get_mqtt_client() + local_tz = datetime.datetime.now().astimezone().tzinfo + self._scheduler = AsyncIOScheduler(timezone=local_tz) + # Prevent scheduler warnings + self._scheduler._logger.setLevel(logging.ERROR) + + self.adapters_checker_job = self._scheduler.add_job( + func=self._testrun.get_net_orc().network_adapters_checker, + kwargs={ + 'mqtt_client': self._mqtt_client, + 'topic': NETWORK_ADAPTERS_TOPIC + }, + trigger='interval', + seconds=CHECK_NETWORK_ADAPTERS_PERIOD, + ) + # add internet connection cheking job only in single-intf mode + if 'single_intf' not in self._testrun.get_session().get_runtime_params(): + self.internet_shecker = self._scheduler.add_job( + func=self._testrun.get_net_orc().internet_conn_checker, + kwargs={ + 'mqtt_client': self._mqtt_client, + 'topic': INTERNET_CONNECTION_TOPIC + }, + trigger='interval', + seconds=CHECK_INTERNET_PERIOD, + ) + + @asynccontextmanager + async def start(self, app: FastAPI): # pylint: disable=unused-argument + """Start background tasks + + Args: + app (FastAPI): app instance + """ + # Job that checks for changes in network adapters + self._scheduler.start() + yield diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py index 88a25a2b1..461f10be3 100644 --- a/framework/python/src/common/testreport.py +++ b/framework/python/src/common/testreport.py @@ -16,15 +16,28 @@ from datetime import datetime from weasyprint import HTML from io import BytesIO -from common import util +from common import util, logger +from common.statuses import TestrunStatus, TestrunResult +from test_orc import test_pack import base64 import os from test_orc.test_case import TestCase +from jinja2 import Environment, FileSystemLoader, BaseLoader +from collections import OrderedDict +from bs4 import BeautifulSoup + DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S' RESOURCES_DIR = 'resources/report' TESTS_FIRST_PAGE = 11 TESTS_PER_PAGE = 20 +TEST_REPORT_STYLES = 'test_report_styles.css' +TEMPLATES_FOLDER = 'report_templates' +TEST_REPORT_TEMPLATE = 'report_template.html' +ICON = 'icon.png' + + +LOGGER = logger.get_logger('REPORT') # Locate parent directory current_dir = os.path.dirname(os.path.realpath(__file__)) @@ -43,27 +56,39 @@ class TestReport(): """Represents a previous Testrun report.""" def __init__(self, - status='Non-Compliant', + result=TestrunResult.NON_COMPLIANT, started=None, finished=None, total_tests=0): self._device = {} self._mac_addr = None - self._status: str = status + self._status: TestrunStatus = TestrunStatus.COMPLETE + self._result: TestrunResult = result self._started = started self._finished = finished self._total_tests = total_tests self._results = [] self._module_reports = [] + self._module_templates = [] self._report_url = '' + self._export_url = '' self._cur_page = 0 + def update_device_profile(self, additional_info): + self._device['device_profile'] = additional_info + def add_module_reports(self, module_reports): self._module_reports = module_reports + def add_module_templates(self, module_templates): + self._module_templates = module_templates + def get_status(self): return self._status + def get_result(self): + return self._result + def get_started(self): return self._started @@ -86,6 +111,9 @@ def set_report_url(self, url): def get_report_url(self): return self._report_url + def get_export_url(self): + return self._export_url + def set_mac_addr(self, mac_addr): self._mac_addr = mac_addr @@ -99,6 +127,7 @@ def to_json(self): report_json['mac_addr'] = self._mac_addr report_json['device'] = self._device report_json['status'] = self._status + report_json['result'] = self._result report_json['started'] = self._started.strftime(DATE_TIME_FORMAT) report_json['finished'] = self._finished.strftime(DATE_TIME_FORMAT) @@ -115,6 +144,10 @@ def to_json(self): if test.recommendations is not None and len(test.recommendations) > 0: test_dict['recommendations'] = test.recommendations + if (test.optional_recommendations is not None + and len(test.optional_recommendations) > 0): + test_dict['optional_recommendations'] = test.optional_recommendations + test_results.append(test_dict) report_json['tests'] = {'total': self._total_tests, @@ -141,12 +174,25 @@ def from_json(self, json_file): if 'test_modules' in json_file['device']: self._device['test_modules'] = json_file['device']['test_modules'] + if 'test_pack' in json_file['device']: + self._device['test_pack'] = json_file['device']['test_pack'] + + if 'additional_info' in json_file['device']: + self._device['device_profile'] = json_file['device']['additional_info'] + self._status = json_file['status'] + + if 'result' in json_file: + self._result = json_file['result'] + self._started = datetime.strptime(json_file['started'], DATE_TIME_FORMAT) self._finished = datetime.strptime(json_file['finished'], DATE_TIME_FORMAT) if 'report' in json_file: self._report_url = json_file['report'] + if 'export' in json_file: + self._export_url = json_file['export'] + self._total_tests = json_file['tests']['total'] # Loop through test results @@ -157,8 +203,16 @@ def from_json(self, json_file): expected_behavior=test_result['expected_behavior'], required_result=test_result['required_result'], result=test_result['result']) + + # Add test recommendations if 'recommendations' in test_result: test_case.recommendations = test_result['recommendations'] + + # Add optional test recommendations + if 'optional_recommendations' in test_result: + test_case.optional_recommendations = test_result[ + 'optional_recommendations'] + self.add_test(test_case) # Create a pdf file in memory and return the bytes @@ -172,955 +226,134 @@ def to_pdf(self): return pdf_bytes def to_html(self): - json_data = self.to_json() - return f''' - - - {self.generate_head()} - - {self.generate_body(json_data)} - - - ''' - - def generate_test_sections(self, json_data): - results = json_data['tests']['results'] - sections = '' - for result in results: - sections += self.generate_test_section(result) - return sections - - def generate_test_section(self, result): - section_content = '
\n' - for key, value in result.items(): - if value is not None: # Check if the value is not None - # Replace underscores and capitalize - formatted_key = key.replace('_', ' ').title() - section_content += f'

{formatted_key}: {value}

\n' - section_content += '
\n
\n' - return section_content - - def generate_pages(self, json_data): - - # Calculate pages - test_count = len(json_data['tests']['results']) - - # Multiple pages required - if test_count > TESTS_FIRST_PAGE: - # First page - full_page = 1 - - # Remaining tests - test_count -= TESTS_FIRST_PAGE - full_page += (int)(test_count / TESTS_PER_PAGE) - partial_page = 1 if test_count % TESTS_PER_PAGE > 0 else 0 - # 1 page required - elif test_count == TESTS_FIRST_PAGE: - full_page = 1 - partial_page = 0 - # Less than 1 page required - else: - full_page = 0 - partial_page = 1 - - num_pages = full_page + partial_page - - pages = '' - for _ in range(num_pages): - self._cur_page += 1 - pages += self.generate_results_page(json_data=json_data, - page_num=self._cur_page) - return pages - - def generate_results_page(self, json_data, page_num): - page = '
' - page += self.generate_header(json_data, (page_num == 1)) - if page_num == 1: - page += self.generate_summary(json_data) - page += self.generate_results(json_data, page_num) - page += self.generate_footer(page_num) - page += '
' - page += '
' - return page - - def generate_module_page(self, json_data, module_report): - self._cur_page += 1 - page = '
' - page += self.generate_header(json_data, False) - page += f''' -
- {module_report} -
''' - page += self.generate_footer(self._cur_page) - page += '
' # Page end - page += '
' - return page - - def generate_steps_to_resolve(self, json_data): - - steps_so_far = 0 - tests_with_recommendations = [] - index = 1 - - # Collect all tests with recommendations - for test in json_data['tests']['results']: - if 'recommendations' in test: - tests_with_recommendations.append(test) - - # Check if test has recommendations - if len(tests_with_recommendations) == 0: - return '' - - # Start new page - self._cur_page += 1 - page = '
' - page += self.generate_header(json_data, False) - - # Add title - page += '

Steps to Resolve

' - - for test in tests_with_recommendations: - - # Generate new page - if steps_so_far == 4 and ( - len(tests_with_recommendations) - (index-1) > 0): - - # Reset steps counter - steps_so_far = 0 - - # Render footer - page += self.generate_footer(self._cur_page) - page += '
' # Page end - page += '
' - - # Render new header - self._cur_page += 1 - page += '
' - page += self.generate_header(json_data, False) - - # Render test recommendations - page += f''' -
-
- {index}. -
- Name
{test["name"]} -
-
- Description
{test["description"]} -
-
-
- Steps to resolve - ''' - - step_number = 1 - for recommendation in test['recommendations']: - page += f''' -
{ - step_number}. {recommendation}''' - step_number += 1 - - page += '
' - - index += 1 - steps_so_far += 1 - - # Render final footer - page += self.generate_footer(self._cur_page) - page += '
' # Page end - page += '
' - - return page - - def generate_module_pages(self, json_data): - pages = '' - content_max_size = 913 - - for module_reports in self._module_reports: - # ToDo: Figure out how to make this dynamic - # Padding values from CSS - # Element sizes from inspection of rendered report - h1_padding = 8 - module_summary_padding = 50 # 25 top and 25 bottom - - # Reset values for each module report - data_table_active = False - data_rows_active = False - page_content = '' - content_size = 0 - content = module_reports.split('\n') - - for line in content: - if '' in line and data_table_active: - data_table_active=False - - # Add module-data header size, ignore rows, should - # only be one so only care about a header existence - elif '' in line and data_table_active: - content_size += 41.333 - - # Track module-data table state - elif '' in line and data_table_active: - data_rows_active = True - elif '' in line and data_rows_active: - data_rows_active = False - - # Add appropriate content size for each data row - # update if CSS changes for this element - elif '' in line and data_rows_active: - content_size += 42 - - # If the current line is within the content size limit - # we'll add it to this page, otherweise, we'll put it on the next - # page. Also make sure that if there is less than 40 pixels - # left after a data row, start a new page or the row will get cut off. - # Current row size is 42 # adjust if we update the - # "module-data tbody tr" element. - if content_size >= content_max_size or ( - data_rows_active and content_max_size - content_size < 42): - # If in the middle of a table, close the table - if data_rows_active: - page_content += '' - page = self.generate_module_page(json_data, page_content) - pages += page + '\n' - content_size = 0 - # If in the middle of a data table, restart - # it for the rest of the rows - page_content = ('\n' - if data_rows_active else '') - page_content += line + '\n' - if len(page_content) > 0: - page = self.generate_module_page(json_data, page_content) - pages += page + '\n' - return pages - - def generate_body(self, json_data): - self._num_pages = 0 - self._cur_page = 0 - body = f''' - - {self.generate_pages(json_data)} - {self.generate_steps_to_resolve(json_data)} - {self.generate_module_pages(json_data)} - - ''' - # Set the max pages after all pages have been generated - return body.replace('MAX_PAGE', str(self._cur_page)) - - def generate_footer(self, page_num): - footer = f''' - - ''' - return footer - - def generate_results(self, json_data, page_num): - - successful_tests = 0 - for test in json_data['tests']['results']: - if test['result'] != 'Error': - successful_tests += 1 - - result_list = f''' -
-

Results List ({successful_tests}/{self._total_tests})

-
-
Name
-
Description
-
Result
-
''' - if page_num == 1: - start = 0 - elif page_num == 2: - start = TESTS_FIRST_PAGE - else: - start = (page_num - 2) * TESTS_PER_PAGE + TESTS_FIRST_PAGE - results_on_page = TESTS_FIRST_PAGE if page_num == 1 else TESTS_PER_PAGE - result_end = min(start + results_on_page, - len(json_data['tests']['results'])) - for ix in range(result_end - start): - result = json_data['tests']['results'][ix + start] - result_list += self.generate_result(result) - result_list += '
' - return result_list - - def generate_result(self, result): - if result['result'] == 'Non-Compliant': - result_class = 'result-test-result-non-compliant' - elif result['result'] == 'Compliant': - result_class = 'result-test-result-compliant' - elif result['result'] == 'Error': - result_class = 'result-test-result-error' - elif result['result'] == 'Feature Not Detected': - result_class = 'result-test-result-feature-not-detected' - elif result['result'] == 'Informational': - result_class = 'result-test-result-informational' - else: - result_class = 'result-test-result-skipped' - - result_html = f''' -
-
{result['name']}
-
{result['description']}
-
{result['result']}
-
- ''' - return result_html - - def generate_header(self, json_data, first_page): + # Obtain test pack + current_test_pack = test_pack.TestPack.get_test_pack( + self._device['test_pack']) + template_folder = os.path.join(current_test_pack.path, + TEMPLATES_FOLDER) + # Jinja template + template_env = Environment( + loader=FileSystemLoader( + template_folder + ), + trim_blocks=True, + lstrip_blocks=True + ) + template = template_env.get_template(TEST_REPORT_TEMPLATE) + + # Report styles + with open(os.path.join(report_resource_dir, + TEST_REPORT_STYLES), + 'r', + encoding='UTF-8' + ) as style_file: + styles = style_file.read() + + # Load Testrun logo to base64 with open(test_run_img_file, 'rb') as f: - tr_img_b64 = base64.b64encode(f.read()).decode('utf-8') - header = '' - - if first_page: - header += f''' -
-

Testrun report

-

- {json_data["device"]["manufacturer"]} - {json_data["device"]["model"]} -

''' - else: - header += f''' -
-

Testrun report

-

- {json_data["device"]["manufacturer"]} - {json_data["device"]["model"]} -

''' - header += f'''Testrun -
- ''' - return header - - def generate_summary(self, json_data): - # Generate the basic content section layout - summary = ''' -
- ''' - # Add the device information - manufacturer = (json_data['device']['manufacturer'] - if 'manufacturer' in json_data['device'] else 'Undefined') - model = (json_data['device']['model'] - if 'model' in json_data['device'] else 'Undefined') - fw = (json_data['device']['firmware'] - if 'firmware' in json_data['device'] else 'Undefined') - mac = (json_data['device']['mac_addr'] - if 'mac_addr' in json_data['device'] else 'Undefined') - - summary += '''
-
''' - - summary += self.generate_device_summary_label('Manufacturer', manufacturer) - summary += self.generate_device_summary_label('Model', model) - summary += self.generate_device_summary_label('Firmware', fw) - summary += self.generate_device_summary_label('MAC Address', - mac, - trailing_space=False) - - summary += '
' - - # Add device configuration - summary += ''' -
-
-

Device Configuration

-
- ''' - - if 'test_modules' in json_data['device']: - - sorted_modules = {} - - for test_module in json_data['device']['test_modules']: - if 'enabled' in json_data['device']['test_modules'][test_module]: - sorted_modules[test_module] = json_data['device']['test_modules'][ - test_module]['enabled'] - - # Sort the modules by enabled first - sorted_modules = sorted(sorted_modules.items(), - key=lambda x:x[1], - reverse=True) - - for module in sorted_modules: - summary += self.generate_device_module_label( - module[0], - module[1] - ) - - summary += '
' + logo = base64.b64encode(f.read()).decode('utf-8') - # Add device configuration - summary += ''' -
-
-

Device Configuration

-
- ''' + # Icon + with open(os.path.join(template_folder, ICON), 'rb') as f: + icon = base64.b64encode(f.read()).decode('utf-8') - if 'test_modules' in json_data['device']: - - sorted_modules = {} - - for test_module in json_data['device']['test_modules']: - if 'enabled' in json_data['device']['test_modules'][test_module]: - sorted_modules[test_module] = json_data['device']['test_modules'][ - test_module]['enabled'] - - # Sort the modules by enabled first - sorted_modules = sorted(sorted_modules.items(), - key=lambda x:x[1], - reverse=True) - - for module in sorted_modules: - summary += self.generate_device_module_label( - module[0], - module[1] - ) - - summary += '
' - - # Add the result summary - summary += self.generate_result_summary(json_data) - - summary += '\n
' - return summary - - def generate_device_module_label(self, module, enabled): - - # Do not render deleted modules - if module == 'nmap': - return '' - - label = '
' - if enabled: - label += '' - else: - label += '' - label += util.get_module_display_name(module) - label += '
' - return label - - def generate_result_summary(self, json_data): - if json_data['status'] == 'Compliant': - result_summary = '''
''' - else: - result_summary = '''
''' - result_summary += self.generate_result_summary_item('Test status', - 'Complete') - result_summary += self.generate_result_summary_item( - 'Test result', - json_data['status'], - style='color: white; font-size:24px; font-weight: 700;') - result_summary += self.generate_result_summary_item('Started', - json_data['started']) + json_data=self.to_json() # Convert the timestamp strings to datetime objects start_time = datetime.strptime(json_data['started'], '%Y-%m-%d %H:%M:%S') end_time = datetime.strptime(json_data['finished'], '%Y-%m-%d %H:%M:%S') + # Calculate the duration duration = end_time - start_time - result_summary += self.generate_result_summary_item('Duration', - str(duration)) - - result_summary += '\n
' - return result_summary - - def generate_result_summary_item(self, key, value, style=None): - summary_item = f'''
{key}
''' - if style is not None: - summary_item += f'''
{value}
''' - else: - summary_item += f'''
{value}
''' - return summary_item - - def generate_device_summary_label(self, key, value, trailing_space=True): - label = f''' -

{key}

-
{value}
- ''' - if trailing_space: - label += '''
''' - return label - - def generate_head(self): - return f''' - - - - Testrun Report - - - ''' - - def generate_css(self): - return ''' - /* Set some global variables */ - :root { - --header-height: .75in; - --header-width: 8.5in; - --header-pos-x: 0in; - --header-pos-y: 0in; - --page-width: 8.5in; - --summary-height: 2.8in; - --vertical-line-height: calc(var(--summary-height)-.2in); - --vertical-line-pos-x: 25%; - } - - @font-face { - font-family: 'Google Sans'; - font-style: normal; - src: url(https://fonts.gstatic.com/s/googlesans/v58/4Ua_rENHsxJlGDuGo1OIlJfC6l_24rlCK1Yo_Iqcsih3SAyH6cAwhX9RFD48TE63OOYKtrwEIJllpyk.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - - @font-face { - font-family: 'Roboto Mono'; - font-style: normal; - src: url(https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - - /* Define some common body formatting*/ - body { - font-family: 'Google Sans', sans-serif; - margin: 0px; - padding: 0px; - } - - /* Use this for various section breaks*/ - .gradient-line { - position: relative; - background-image: linear-gradient(to right, red, blue, green, yellow, orange); - height: 1px; - /* Adjust the height as needed */ - width: 100%; - /* To span the entire width */ - display: block; - /* Ensures it's a block-level element */ - } - - /* Sets proper page size during print to pdf for weasyprint */ - @page { - size: Letter; - width: 8.5in; - height: 11in; - } - - .page { - position: relative; - margin: 0 20px; - width: 8.5in; - height: 11in; - } - - /* Define the header related css elements*/ - .header { - position: relative; - } - - h1 { - margin: 0 0 8px 0; - font-size: 20px; - font-weight: 400; - } - - h2 { - margin: 0px; - font-size: 48px; - font-weight: 700; - } - - h3 { - font-size: 24px; - } - - h4 { - font-size: 12px; - font-weight: 500; - color: #5F6368; - margin-bottom: 0; - margin-top: 0; - } - - .module-summary { - background-color: #F8F9FA; - width: 100%; - margin-bottom: 25px; - margin-top: 25px; - } - - .module-summary thead tr th { - text-align: left; - padding-top: 15px; - padding-left: 15px; - font-weight: 500; - color: #5F6368; - font-size: 14px; - } - - .module-summary tbody tr td { - padding-bottom: 15px; - padding-left: 15px; - font-size: 24px; - } - - .module-data { - border: 1px solid #DADCE0; - border-radius: 3px; - border-spacing: 0; - } - .module-data thead tr th { - text-align: left; - padding: 12px 25px; - color: #3C4043; - font-size: 14px; - font-weight: 700; - } - - .module-data tbody tr td { - text-align: left; - padding: 12px 25px; - color: #3C4043; - font-size: 14px; - font-weight: 400; - border-top: 1px solid #DADCE0; - font-family: 'Roboto Mono', monospace; - } - - div.steps-to-resolve { - background-color: #F8F9FA; - margin-bottom: 30px; - width: 756px; - padding: 20px 30px; - vertical-align: top; - } - - .steps-to-resolve-row { - vertical-align: top; - } - - .steps-to-resolve-test-name { - display: inline-block; - margin-left: 70px; - margin-bottom: 20px; - width: 250px; - vertical-align: top; - } - - .steps-to-resolve-description { - display: inline-block; - } - - .steps-to-resolve.subtitle { - text-align: left; - padding-top: 15px; - font-weight: 500; - color: #5F6368; - font-size: 14px; - } - - .steps-to-resolve-index { - font-size: 40px; - position: absolute; - } - - .callout-container.info { - background-color: #e8f0fe; - } - - .callout-container.info .icon { - width: 22px; - height: 22px; - margin-right: 5px; - background-size: contain; - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEwAAABOCAYAAACKX/AgAAAABHNCSVQICAgIfAhkiAAACYVJREFUeF7tXGtsVEUUPi0t0NIHli5Uni1I5KVYiCgPtQV8BcSIBkVUjFI0GiNGhR9KiIEfIqIkRlSqRlBQAVEREx9AqwIqClV5imILCBT6gHZLW2gLnm+xZHM5d2fm7t1tN9kv2R+dO3fmzHfncV7TmNKTZ89RFNoMxGrXjFb0MRAlzHAiRAmLEmbIgGH16AyLEmbIgGH16AyLEmbIgGH1OMP6rlVvZH1518E62nO4jkrKz9CBstNU4W2kU6fP8q/J10+Hdm34F0udkuOol6cdZXnaUr+uCTSwZwLFxca4JotJQzHh1PS9dU307Y4q2rjTS0XFp6j2zFkTWS/UTWwbS9m9O9CYgck09spUSm7fxlE7Tl4KC2F/H6un/PVlVLC7mhoa3bXE4uNiKHdACk0f66E+Xdo74cDonZASdryqgV7/5jit23aCQm2xtuElOn5IR3rsps7UOTXeiASTyiEhDEvv3cJyWrG5nM40uDujVINrFxdLk0d1oody0ik5wf2l6jphW/+uoZnLD1FV7fmNWzVA6Xnzfh7MrOzYIY7mT+lOw/okSV04LnOVsI+3VNDLX5QSTkAdJPEJOLJfCg3JSvAtI08y/1LjKC3p/OFdWdNIZVX88zYQlve24lrastdLNXyS6gAn6bMTMmjS8E461bXquEJYQ9M5mv/5Ufrk50plpyBjzKAUyuETbljvJIrjTdsEjXxobP2nhgp3eWnDzmoCqSrcdU0azbz9UopvY9aX1G7QhFXz0ntq2UHazmpCIECfmnpDOt1/fTq1j3fHwKhjteT978tpGf+gvwXCUFZDXrm/J6UkBrevBUUYZtaj+SUBycJXnchf+JExHrrk/6UWaGBOnmGWLdlQRp/8VBlwOwBpb0zLDGqmBUXYvDVHAi7DjI7xtGhqL7q8a+j1IxC990gdzXjvIB3j/c4OWJ7PTexq91hZ7nhtfLS5IiBZV2Um0vIn+oSNLIwUZhP6HMx922E177Mrf6ywe6wsd0TYz3/V0MJ1pbaNTxh6CeXnZV047WwrhuAB7M63uW/IYIcFa0tp6/4au8cBy40Jg1I6a8Uh270Cgr4wqZvx6RdQSsOHOHkhgx1pUHtmLf+XMBZTGBP2TkG5rVKKpTA7iP0Bpx4Mcv8fypwCstgtz5OnGn3WiCmMNn1sphMW7BPNHWzw2D+alU5TQVB/xOzdZCUogT0TW+YOcNKc7x24jKa8tl88CGBGrZ3Z18j2NJphi78+LpIF1QGnYTBkOWZE8SL2tEUP9hT9Z6cbz9Jidg6YQJuwv0rrad32E2Lbd16bFtbTUBQiQCFOT8goYd32k7Sf3U+60CYsnxVDyUSEBj99tEe3vxarN50VZ8hqRRMPagn76nRxcQvCmzhNCtn5JwHmTqg0eKk/p2XYLh5gs0wCHJveer0TU4uwr36vEj2lEAK2YaQAskr7LLzA6/+o0hqGVhCkYJc8u8ZekeqaIQ1pgzkNdUaLExeeklVsc1qxgb0fdwyT9zn/usoZBgO7iP1QEnIGJEvFrboMbiUJRf+cslXGjQjbeaiW6hsuVh7h/Luarf9IA3xwkN0KKMsI+6lw8ZuWNxA3lDCqf0qLmj+STDplMJtG9JNnGbwdKigJO1Amu0qyMxNUbbfa50OzZG9GcdkZpcxKwko4Ii0BplCkwi4Mh+i7CspTEraYBE+K+4SNnbeXai2u5kTeb9Y/308SwXEZgi0S7MbqX1dJWHOeg7WDdJtOrfVM/gZZVuPb5H3duohMSVDFBfCOcklK+Q+IG6YlBRdMkAQOVxmUVymXxW5y+MulJOycEGKMiQk+XBUuctzuR0mYncFaWaNne7ktsBvtIcokOxLUq0aDMLmRco5GRyoQTZcgTQ5rPSVhcMBJKKuOYMJsPrbdWP3HryQskzP/JByz+UpS3dZWhjwNCchyVEFJWC+PrLMUlcgGuarD1vB8e7FsAmWmt1WKpySsfzfZBNq0x0vwZEQakMyyea/srrIbq/8YlYRd0T1R9HnBQ7mNXSKRBmT+SOlSyJtFsrEKSsJg3SPsL6GAnW6RBqRJScjO6iBGlqx1lYThhdHspZSwnjOiJV+ZVLc1lEFW5JRJGD1IdvlY62oRdsvgVNH3BQXwgx/Mo8dWIcL1N3LJpAQ8ZGLfyO52HWgRhuTaHHYYSljK4XaE3Vs7TvDHXfqd/HGRtq6bQKxFGMhAHrxksGIDXbJRP67XUsS+xXFVyRuBMeXx2HShTVjfjPY0boicQrT6x0rad1Q/eqwrnFv1/jxST2ts8m/Hc7bRZQYXIrQJg/C4NID1bgX0sRnvHRD3B2vdcP+NPWvG0gOiztg2PoYe5zGZwCh7Bw0v+rKUlvLmKQHqBxLpTDOjm9uC89CqCuPzIJ7oBFBS8/KL6Tcbq+TBHA89eWsXo6aNJXko12ObiQzB5n56xEgA/8ogBgqk/88pWWh3Lufg2pGVytnUuC1iCmPCkLY9/94ehLs9Etb+eoLmrDpM+LotBfQ9Z+VhWst3nCTgwsNLU3pon4z+bRgThpev7ZtET4/LkGTxlYE0LAVJ57F9yaUH6HMa921HFrp55rYMGnaZsys1jghDp7gANTFALgKWwn2c+RfO0xOnIbINf7fZsyD3nZx2fvcI51dpjDd9/4mge7HhruFpvhwyXJgKBaCUQhfExYZAHpQhbC++mdeCFxsweN2rM8hnmMqb7H3XuXd1BrYhzB1o8JJS6v9xQNarD7Tw1ZlmgfBVX/zsKK3ZenEakXVGIcSFNKlczqLBVRbTC1PY0H9ht1Lhbi/B+NfZJ7EMZ7WWy1n+hHy4qYIWsp6GNEgd4K72qP7JlM36WxcOriKajgBxc8wTkSkEWxA/KD3ZQEUldbRpT7Xoz5L6w2n49PgMumek8z3L2m5Qe5i1Mfz9E98SwcUHLFWngMpyjgOimryL3UDPgvpzDZ/obsJ1wiAcyHq3oIxW8IVTty/FqwYPc2fyiHR6ODdCrjD7DwjLCHnwX3K6ejCzRUUSnkOPHs/Ogcdu7szLWw7c6LSjqhOSGWbtFDn+SO0u5P3HbQsAzoAc9mflcVo5PCqhRlgIax4E0teRkb2R3cRQbJ26t3GjN5uT4nIHphC8wbrOPzfIDCth/gJjpu34t9b3r2SQ5YjEvfP/SqbJp1Mh3wVGOP6dDCLSCCgjRopQ2KAeicbqiBtkoY0WI8ytAYS7Hce2ZLgFbS39RQkz/BJRwqKEGTJgWD06w6KEGTJgWD06w6KEGTJgWP0/nqir/+GPk3oAAAAASUVORK5CYII='); - } - - .callout-container { - display: flex; - box-sizing: border-box; - height: auto; - min-height: 48px; - padding: 6px 24px; - border-radius: 8px; - align-items: center; - gap: 10px; - color: #3c4043; - font-size: 14px; - } - - .device-information { - padding-top: 0.2in; - padding-left: 0.2in; - background-color: #F8F9FA; - width: 250px; - height: 100.4%; - } - - /* Define the summary related css elements*/ - .summary-content { - position: relative; - width: var(--page-width); - height: var(--summary-height); - margin-top: 19px; - margin-bottom: 19px; - background-color: #E8EAED; - padding-bottom: 20px; - } - - .summary-item-label { - position: relative; - } - - .summary-item-value { - position: relative; - font-size: 20px; - font-weight: 400; - color: #202124; - } - - .summary-item-space { - position: relative; - padding-bottom: 15px; - margin: 0; - } - - .summary-device-modules { - position: absolute; - left: 3.2in; - top: .3in; - } - - .summary-device-module-label { - font-size: 16px; - font-weight: 500; - color: #202124; - width: fit-content; - margin-bottom: 0.1in; - } - - .summary-vertical-line { - width: 1px; - height: var(--vertical-line-height); - background-color: #80868B; - position: absolute; - top: .3in; - bottom: .1in; - left: 3in; - } - - /* CSS for the color box */ - .summary-color-box { - position: absolute; - right: 0in; - top: 0in; - width: 2.6in; - height: 100%; - } - - .summary-box-compliant { - background-color: rgb(24, 128, 56); - } - - .summary-box-non-compliant { - background-color: #b31412; - } - - .summary-box-label { - font-size: 14px; - margin-top: 5px; - color: #DADCE0; - position: relative; - top: 10px; - left: 20px; - font-weight: 500; - } - - .summary-box-value { - font-size: 18px; - margin: 0 0 10px 0; - color: #ffffff; - position: relative; - top: 10px; - left: 20px; - } - - .result-list-title { - font-size: 24px; - } - - .result-list { - position: relative; - margin-top: .2in; - font-size: 18px; - } - - .result-line { - border: 1px solid #D3D3D3; - /* Light Gray border*/ - height: .4in; - width: 8.5in; - } - - .result-line-result { - border-top: 0px; - } - - .result-list-header-label { - font-weight: 500; - position: absolute; - font-size: 12px; - font-weight: bold; - height: 40px; - display: flex; - align-items: center; - } - - .result-test-label { - position: absolute; - font-size: 12px; - margin-top: 12px; - max-width: 300px; - font-weight: normal; - align-items: center; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - - .result-test-description { - max-width: 380px; - } - - .result-test-result-error { - background-color: #FCE8E6; - color: #C5221F; - left: 7.3in; - } - - .result-test-result-feature-not-detected { - background-color: #e3e3e3; - left: 6.85in; - } - - .result-test-result-informational { - background-color: #d9f0ff; - color: #0b5c8d; - left: 7.08in; - } - - .result-test-result-non-compliant { - background-color: #FCE8E6; - color: #C5221F; - left: 7.01in; - } - - .result-test-result { - position: absolute; - font-size: 12px; - width: fit-content; - height: 12px; - margin-top: 8px; - padding: 4px 4px 7px 5px; - border-radius: 2px; - } - - .result-test-result-compliant { - background-color: #E6F4EA; - color: #137333; - left: 7.16in; - } + # Calculate number of successful tests + successful_tests = 0 + for test in json_data['tests']['results']: + if test['result'] != 'Error': + successful_tests += 1 - .result-test-result-skipped { - background-color: #e3e3e3; - color: #393939; - left: 7.24in; - } + # Obtain the steps to resolve + logic = current_test_pack.get_logic() + steps_to_resolve_ = logic.get_steps_to_resolve(json_data) + + module_reports = self._module_reports + env_module = Environment(loader=BaseLoader()) + pages_num = self._pages_num(json_data) + module_templates = [ + env_module.from_string(s).render( + name=current_test_pack.name, + device=json_data['device'], + logo=logo, + icon=icon, + version=self._version, + ) for s in self._module_templates + ] + + return self._add_page_counter(template.render(styles=styles, + logo=logo, + icon=icon, + version=self._version, + json_data=json_data, + device=json_data['device'], + modules=self._device_modules(json_data['device']), + test_status=json_data['status'], + duration=duration, + successful_tests=successful_tests, + total_tests=self._total_tests, + test_results=json_data['tests']['results'], + steps_to_resolve=steps_to_resolve_, + module_reports=module_reports, + pages_num=pages_num, + tests_first_page=TESTS_FIRST_PAGE, + tests_per_page=TESTS_PER_PAGE, + module_templates=module_templates + )) + + def _add_page_counter(self, html): + # Add page nums and total page + soup = BeautifulSoup(html, features='html5lib') + page_index_divs = soup.find_all('div', class_='page-index') + total_pages = len(page_index_divs) + for index, div in enumerate(page_index_divs): + div.string = f'Page {index+1}/{total_pages}' + return str(soup) + + def _pages_num(self, json_data): - /* CSS for the footer */ - .footer { - position: absolute; - height: 30px; - width: 8.5in; - bottom: 0in; - border-top: 1px solid #D3D3D3; - } + # Calculate pages + test_count = len(json_data['tests']['results']) - .footer-label { - color: #3C4043; - position: absolute; - top: 5px; - font-size: 12px; - } + # Multiple pages required + if test_count > TESTS_FIRST_PAGE: + # First page + pages = 1 - /*CSS for the markdown tables */ - .markdown-table { - border-collapse: collapse; - margin-left: 20px; - background-color: #F8F9FA; - } + # Remaining testsgenerate + test_count -= TESTS_FIRST_PAGE + pages += (int)(test_count / TESTS_PER_PAGE) + pages = pages + 1 if test_count % TESTS_PER_PAGE > 0 else pages - .markdown-table th, .markdown-table td { - border: none; - text-align: left; - padding: 8px; - } + # 1 page required + else: + pages = 1 - .markdown-header-h1 { - margin-top:20px; - margin-bottom:20px; - margin-right:0px; - font-size: 2em; - } + return pages - .markdown-header-h2 { - margin-top:20px; - margin-bottom:20px; - margin-right:0px; - font-size: 1.5em; - } + def _device_modules(self, device): + sorted_modules = {} - .module-page-content { - /*Page height minus header(93px), footer(30px), - and a 20px bottom padding.*/ - height: calc(11in - 93px - 30px - 20px); - - /* In case we mess something up in our calculations - we'll cut off the content of the page so - the header, footer and line break work - as expected - */ - overflow: hidden; - } + if 'test_modules' in device: - .module-page-content h1 { - font-size: 32px; - } + for test_module in device['test_modules']: + if 'enabled' in device['test_modules'][test_module]: + sorted_modules[ + util.get_module_display_name(test_module)] = device['test_modules'][ + test_module]['enabled'] - @media print { - @page { - size: Letter; - width: 8.5in; - height: 11in; - } - }''' + # Sort the modules by enabled first + sorted_modules = OrderedDict(sorted(sorted_modules.items(), + key=lambda x:x[1], + reverse=True) + ) + return sorted_modules diff --git a/framework/python/src/common/util.py b/framework/python/src/common/util.py index 096aaf4df..7bb7ea73f 100644 --- a/framework/python/src/common/util.py +++ b/framework/python/src/common/util.py @@ -12,18 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Provides basic utilities for the network orchestrator.""" +"""Provides basic utilities for Testrun.""" import getpass import os import subprocess import shlex -from common import logger +import typing as t import netifaces +from common import logger LOGGER = logger.get_logger('util') -def run_command(cmd, output=True): +def run_command(cmd, output=True, timeout=None, supress_error=False): """Runs a process at the os level By default, returns the standard output and error output If the caller sets optional output parameter to False, @@ -35,9 +36,9 @@ def run_command(cmd, output=True): with subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process: - stdout, stderr = process.communicate() + stdout, stderr = process.communicate(timeout) - if process.returncode != 0 and output: + if process.returncode != 0 and output and not supress_error: err_msg = f'{stderr.strip()}. Code: {process.returncode}' LOGGER.error('Command failed: ' + cmd) LOGGER.error('Error: ' + err_msg) @@ -51,12 +52,15 @@ def run_command(cmd, output=True): def interface_exists(interface): + """Checks whether an interface is available""" return interface in netifaces.interfaces() def prettify(mac_string): + """Formats a MAC address with colons""" return ':'.join([f'{ord(b):02x}' for b in mac_string]) def get_host_user(): + """Returns the username of the host user""" user = get_os_user() # If primary method failed, try secondary @@ -66,6 +70,7 @@ def get_host_user(): return user def get_os_user(): + """Attempts to get the username using os library""" user = None try: user = os.getlogin() @@ -78,6 +83,7 @@ def get_os_user(): return user def get_user(): + """Attempts to get the host user using the getpass library""" user = None try: user = getpass.getuser() @@ -96,9 +102,11 @@ def get_user(): return user def set_file_owner(path, owner): + """Change the owner of a file""" run_command(f'chown -R {owner} {path}') def get_module_display_name(search): + """Returns the display name of a test module""" modules = { 'ntp': 'NTP', 'dns': 'DNS', @@ -113,3 +121,32 @@ def get_module_display_name(search): return module[1] return 'Unknown' + + +def diff_dicts(d1: t.Dict[t.Any, t.Any], d2: t.Dict[t.Any, t.Any]) -> t.Dict: + """Compares two dictionaries by keys + + Args: + d1 (t.Dict[t.Any, t.Any]): first dict to compare + d2 (t.Dict[t.Any, t.Any]): second dict to compare + + Returns: + t.Dict[t.Any, t.Any]: Returns an empty dictionary + if the compared dictionaries are equal, + otherwise returns a dictionary that contains + the removed items(if available) + and the added items(if available). + """ + diff = {} + if d1 != d2: + s1 = set(d1) + s2 = set(d2) + keys_removed = s1 - s2 + keys_added = s2 - s1 + items_removed = {k:d1[k] for k in keys_removed} + items_added = {k:d2[k] for k in keys_added} + if items_removed: + diff['items_removed'] = items_removed + if items_added: + diff['items_added'] = items_added + return diff diff --git a/framework/python/src/core/docker/docker_module.py b/framework/python/src/core/docker/docker_module.py new file mode 100644 index 000000000..d91331aee --- /dev/null +++ b/framework/python/src/core/docker/docker_module.py @@ -0,0 +1,188 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Represents the base module.""" +import docker +from docker.models.containers import Container +import os +from common import logger +import json + +IMAGE_PREFIX = 'testrun/' +CONTAINER_PREFIX = 'tr-ct' +DEFAULT_NETWORK = 'bridge' +DEFAULT_LOG_LEVEL = 'INFO' + +class Module: + """Represents the base module.""" + + def __init__(self, + module_config_file, + session, + docker_network=DEFAULT_NETWORK, + extra_hosts=None): + self._session = session + self.extra_hosts = extra_hosts + self.log_level=DEFAULT_LOG_LEVEL + + # Read the config file into a json object + with open(module_config_file, encoding='UTF-8') as config_file: + module_json = json.load(config_file) + + self.docker_network = docker_network + # General module information + self.name = module_json['config']['meta']['name'] + self.display_name = module_json['config']['meta']['display_name'] + self.description = module_json['config']['meta']['description'] + self.enabled = module_json['config'].get('enabled', True) + self.depends_on = module_json['config']['docker'].get('depends_on', None) + + # Absolute path + # Store the root directory of Testrun based on the expected locatoin + # Testrun/modules///conf -> 5 levels + self.root_path = os.path.abspath( + os.path.join(module_config_file, '../../../../..')) + self.dir = os.path.dirname(os.path.dirname(module_config_file)) + self.dir_name = os.path.basename(self.dir) + + # Docker settings + self.build_file = self.dir_name + '.Dockerfile' + self.image_name = f'{IMAGE_PREFIX}{self.dir_name}' + self.container_name = f'{CONTAINER_PREFIX}-{self.dir_name}' + if 'tests' in module_json['config']: + # Append Test module + self.image_name += '-test' + self.container_name += '-test' + self.enable_container = module_json['config']['docker'].get( + 'enable_container', True) + self.container: Container = None + + # Configure the module logger + self._add_logger(log_name=self.name, module_name=self.name) + try: + self.log_level = self._get_module_log_level(module_json) + self.logger.setLevel(self.log_level) + except Exception as error: + self.logger.error('Could not set defined log level') + self.logger.error(error) + + self.setup_module(module_json) + + def _add_logger(self, log_name, module_name, log_dir=None): + self.logger = logger.get_logger( + name=f'{log_name}_module', # pylint: disable=E1123 + log_file=f'{module_name}_module', + log_dir=log_dir) + + def build(self): + self.logger.debug('Building module ' + self.dir_name) + client = docker.from_env() + client.images.build( + dockerfile=os.path.join(self.dir, self.build_file), + path=self._path, + forcerm=True, # Cleans up intermediate containers during build + tag=self.image_name) + + def get_container(self): + container = None + try: + client = docker.from_env() + container = client.containers.get(self.container_name) + except docker.errors.NotFound: + self.logger.debug('Container ' + self.container_name + ' not found') + except docker.errors.APIError as error: + self.logger.error('Failed to resolve container') + self.logger.error(error) + return container + + def get_session(self): + return self._session + + def get_status(self): + self.container = self.get_container() + if self.container is not None: + return self.container.status + return None + + def get_network(self): + return self.docker_network + + def get_mounts(self): + return [] + + def get_environment(self, device=None): # pylint: disable=W0613 + return {} + + def _get_module_log_level(self, module_json): + log_level = DEFAULT_LOG_LEVEL + try: + test_modules = self.get_session().get_config().get('test_modules', {}) + test_config = test_modules.get(self.name, {}) + sys_log_level = test_config.get('log_level', None) + + if sys_log_level is not None: + log_level = sys_log_level + elif 'log_level' in module_json['config']: + log_level = module_json['config']['log_level'] + except Exception: # pylint: disable=W0718 + # Ignore errors, just use default + log_level = DEFAULT_LOG_LEVEL + return log_level # pylint: disable=W0150 + + def setup_module(self, module_json): + pass + + def _setup_runtime(self, device=None): + pass + + def start(self, device=None): + self._setup_runtime(device) + + self.logger.debug('Starting module ' + self.display_name) + network = self.get_network() + self.logger.debug(f"""Network: {network}, image name: {self.image_name}, + container name: {self.container_name}""") + + try: + client = docker.from_env() + self.container = client.containers.run( + self.image_name, + auto_remove=True, + cap_add=['NET_ADMIN'], + name=self.container_name, + hostname=self.container_name, + network_mode=network, + privileged=True, + detach=True, + mounts=self.get_mounts(), + environment=self.get_environment(device), + extra_hosts=self.extra_hosts if self.extra_hosts is not None else {}) + except docker.errors.ContainerError as error: + self.logger.error('Container run error') + self.logger.error(error) + + def stop(self, kill=False): + self.logger.debug('Stopping module ' + self.container_name) + try: + container = self.get_container() + if container is not None: + if kill: + self.logger.debug('Killing container: ' + self.container_name) + container.kill() + else: + self.logger.debug('Stopping container: ' + self.container_name) + container.stop() + self.logger.debug('Container stopped: ' + self.container_name) + except Exception as error: # pylint: disable=W0703 + self.logger.error('Container stop error') + self.logger.error(error) diff --git a/framework/python/src/core/docker/network_docker_module.py b/framework/python/src/core/docker/network_docker_module.py new file mode 100644 index 000000000..83824dd68 --- /dev/null +++ b/framework/python/src/core/docker/network_docker_module.py @@ -0,0 +1,99 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Represents a test module.""" +from core.docker.docker_module import Module +import os +from docker.types import Mount + +RUNTIME_DIR = 'runtime' +RUNTIME_TEST_DIR = os.path.join(RUNTIME_DIR, 'test') +DEFAULT_TIMEOUT = 60 # time in seconds +DEFAULT_DOCKER_NETWORK = 'none' + + +class NetworkModule(Module): + """Represents a test module.""" + + def __init__(self, module_config_file, session): + super().__init__(module_config_file=module_config_file, + docker_network=DEFAULT_DOCKER_NETWORK, + session=session) + + def setup_module(self, module_json): + self.template = module_json['config']['docker'].get('template', False) + self.net_config = NetworkModuleNetConfig() + if self.enable_container: + self.net_config.enable_wan = module_json['config']['network'].get( + 'enable_wan', False) + self.net_config.host = module_json['config']['network'].get('host', False) + # Override default network if host is requested + if self.net_config.host: + self.docker_network = 'host' + + if not self.net_config.host: + self.net_config.ip_index = module_json['config']['network'].get( + 'ip_index') + + self.net_config.ipv4_address = self.get_session().get_ipv4_subnet()[ + self.net_config.ip_index] + self.net_config.ipv4_network = self.get_session().get_ipv4_subnet() + + self.net_config.ipv6_address = self.get_session().get_ipv6_subnet()[ + self.net_config.ip_index] + + self.net_config.ipv6_network = self.get_session().get_ipv6_subnet() + + self._mounts = [] + if 'mounts' in module_json['config']['docker']: + for mount_point in module_json['config']['docker']['mounts']: + self._mounts.append( + Mount(target=mount_point['target'], + source=os.path.join(os.getcwd(), mount_point['source']), + type='bind')) + + def _setup_runtime(self, device): + pass + + def get_environment(self, device=None): # pylint: disable=W0613 + environment = { + 'TZ': self.get_session().get_timezone(), + 'HOST_USER': self.get_session().get_host_user(), + 'LOG_LEVEL': self.log_level + } + return environment + + def get_mounts(self): + return self._mounts + + +class NetworkModuleNetConfig: + """Define all the properties of the network config for a network module""" + + def __init__(self): + + self.enable_wan = False + + self.ip_index = 0 + self.ipv4_address = None + self.ipv4_network = None + self.ipv6_address = None + self.ipv6_network = None + + self.host = False + + def get_ipv4_addr_with_prefix(self): + return format(self.ipv4_address) + '/' + str(self.ipv4_network.prefixlen) + + def get_ipv6_addr_with_prefix(self): + return format(self.ipv6_address) + '/' + str(self.ipv6_network.prefixlen) diff --git a/framework/python/src/core/docker/test_docker_module.py b/framework/python/src/core/docker/test_docker_module.py new file mode 100644 index 000000000..1235cf25d --- /dev/null +++ b/framework/python/src/core/docker/test_docker_module.py @@ -0,0 +1,159 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Represents a test module.""" +from core.docker.docker_module import Module +from test_orc.test_case import TestCase +import os +import json +from common import util +from docker.types import Mount + +RUNTIME_DIR = 'runtime' +RUNTIME_TEST_DIR = os.path.join(RUNTIME_DIR, 'test') +DEFAULT_TIMEOUT = 60 # time in seconds + + +class TestModule(Module): + """Represents a test module.""" + + def __init__(self, module_config_file, test_orc, session, extra_hosts): + super().__init__(module_config_file=module_config_file, + session=session, + extra_hosts=extra_hosts) + + self._test_orc = test_orc + + # Set IP Index for all test modules + self.ip_index = 9 + + def setup_module(self, module_json): + # Set the defaults + self.network = True + self.total_tests = 0 + self.tests: list = [] + + self.timeout = self._get_module_timeout(module_json) + + # Determine if this module needs network access + if 'network' in module_json['config']: + self.network = module_json['config']['network'] + + # Load test cases + if 'tests' in module_json['config']: + self.total_tests = len(module_json['config']['tests']) + for test_case_json in module_json['config']['tests']: + try: + test_case = TestCase( + name=test_case_json['name'], + description=test_case_json['test_description'], + expected_behavior=test_case_json['expected_behavior']) + + # Check if steps to resolve have been specified + if 'recommendations' in test_case_json: + test_case.recommendations = test_case_json['recommendations'] + + self.tests.append(test_case) + except Exception as error: # pylint: disable=W0718 + self.logger.error('Failed to load test case. See error for details') + self.logger.error(error) + + def _setup_runtime(self, device): + self.device_test_dir = os.path.join(self.root_path, RUNTIME_TEST_DIR, + device.mac_addr.replace(':', '')) + + self.container_runtime_dir = os.path.join(self.device_test_dir, self.name) + os.makedirs(self.container_runtime_dir, exist_ok=True) + + self.container_log_file = os.path.join(self.container_runtime_dir, + 'module.log') + + self.config_file = os.path.join(self.root_path, 'local/system.json') + self.root_certs_dir = os.path.join(self.root_path, 'local/root_certs') + + self.network_runtime_dir = os.path.join(self.root_path, 'runtime/network') + + self.device_startup_capture = os.path.join(self.device_test_dir, + 'startup.pcap') + host_user = self.get_session().get_host_user() + util.run_command(f'chown -R {host_user} {self.device_startup_capture}') + + self.device_monitor_capture = os.path.join(self.device_test_dir, + 'monitor.pcap') + util.run_command(f'chown -R {host_user} {self.device_monitor_capture}') + + def get_environment(self, device): + + # Obtain the test pack + test_pack = self._test_orc.get_test_pack(device.test_pack) + + environment = { + 'TZ': self.get_session().get_timezone(), + 'HOST_USER': self.get_session().get_host_user(), + 'DEVICE_MAC': device.mac_addr, + 'IPV4_ADDR': device.ip_addr, + 'DEVICE_TEST_MODULES': json.dumps(device.test_modules), + 'DEVICE_TEST_PACK': json.dumps(test_pack.to_dict()), + 'IPV4_SUBNET': self.get_session().get_ipv4_subnet(), + 'IPV6_SUBNET': self.get_session().get_ipv6_subnet(), + 'DEV_IFACE': self.get_session().get_device_interface(), + 'DEV_IFACE_MAC': self.get_session().get_device_interface_mac_addr(), + 'LOG_LEVEL': self.log_level + } + return environment + + def get_mounts(self): + mounts = [ + Mount(target='/testrun/system.json', + source=self.config_file, + type='bind', + read_only=True), + Mount(target='/testrun/root_certs', + source=self.root_certs_dir, + type='bind', + read_only=True), + Mount(target='/runtime/output', + source=self.container_runtime_dir, + type='bind'), + Mount(target='/runtime/network', + source=self.network_runtime_dir, + type='bind', + read_only=True), + Mount(target='/runtime/device/startup.pcap', + source=self.device_startup_capture, + type='bind', + read_only=True), + Mount(target='/runtime/device/monitor.pcap', + source=self.device_monitor_capture, + type='bind', + read_only=True) + ] + return mounts + + def _get_module_timeout(self, module_json): + timeout = DEFAULT_TIMEOUT + try: + timeout = DEFAULT_TIMEOUT + test_modules = self.get_session().get_config().get('test_modules', {}) + test_config = test_modules.get(self.name, {}) + sys_timeout = test_config.get('timeout', None) + + if sys_timeout is not None: + timeout = sys_timeout + elif 'timeout' in module_json['config']['docker']: + timeout = module_json['config']['docker']['timeout'] + except Exception: # pylint: disable=W0718 + # Ignore errors, just use default + timeout = DEFAULT_TIMEOUT + return timeout # pylint: disable=W0150 + \ No newline at end of file diff --git a/framework/python/src/common/session.py b/framework/python/src/core/session.py similarity index 51% rename from framework/python/src/common/session.py rename to framework/python/src/core/session.py index f555a9732..16c8f056b 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/core/session.py @@ -17,8 +17,11 @@ import pytz import json import os -from common import util, logger +from fastapi.encoders import jsonable_encoder +from common import util, logger, mqtt from common.risk_profile import RiskProfile +from common.statuses import TestrunStatus, TestResult, TestrunResult +from net_orc.ip_control import IPControl # Certificate dependencies from cryptography import x509 @@ -34,23 +37,66 @@ API_URL_KEY = 'api_url' API_PORT_KEY = 'api_port' MAX_DEVICE_REPORTS_KEY = 'max_device_reports' +ORG_NAME_KEY = 'org_name' +TEST_CONFIG_KEY = 'test_modules' +ALLOW_DISCONNECT_KEY='allow_disconnect' CERTS_PATH = 'local/root_certs' CONFIG_FILE_PATH = 'local/system.json' -SECONDS_IN_YEAR = 31536000 +STATUS_TOPIC = 'status' + +MAKE_CONTROL_DIR = 'make/DEBIAN/control' PROFILE_FORMAT_PATH = 'resources/risk_assessment.json' PROFILES_DIR = 'local/risk_profiles' LOGGER = logger.get_logger('session') - +STATUSES_COMPLETE = (TestrunStatus.CANCELLED, + TestrunStatus.COMPLETE, + TestrunStatus.DO_NOT_PROCEED, + TestrunStatus.PROCEED, + TestrunStatus.IDLE + ) + +def session_tracker(method): + """Session changes tracker.""" + def wrapper(self, *args, **kwargs): + + result = method(self, *args, **kwargs) + + if self.get_status() != TestrunStatus.IDLE and not self.pause_message: + self.get_mqtt_client().send_message( + STATUS_TOPIC, + jsonable_encoder(self.to_json()) + ) + if self.get_status() in STATUSES_COMPLETE: + self.pause_message = True + + return result + return wrapper + +def apply_session_tracker(cls): + """Applies tracker decorator to class methods""" + for attr in dir(cls): + if (callable(getattr(cls, attr)) + and not attr.startswith('_') + and not attr.startswith('get') + and not attr == 'to_json' + ): + setattr(cls, attr, session_tracker(getattr(cls, attr))) + return cls + +@apply_session_tracker class TestrunSession(): - """Represents the current session of Test Run.""" + """Represents the current session of Testrun.""" def __init__(self, root_dir): self._root_dir = root_dir - self._status = 'Idle' + self.pause_message = False + self._status = TestrunStatus.IDLE + self._result = None + self._description = None # Target test device self._device = None @@ -65,6 +111,9 @@ def __init__(self, root_dir): # All historical reports self._module_reports = [] + # Module report templates + self._module_templates = [] + # Parameters specified when starting Testrun self._runtime_params = [] @@ -77,6 +126,9 @@ def __init__(self, root_dir): # Direct url for PDF report self._report_url = None + # Export URL + self._export_url = None + # Version self._load_version() @@ -93,11 +145,20 @@ def __init__(self, root_dir): self._config_file = os.path.join(root_dir, CONFIG_FILE_PATH) self._config = self._get_default_config() + # System network interfaces + self._ifaces = {} # Loading methods self._load_version() self._load_config() self._load_profiles() + # Network information + self._ipv4_subnet = None + self._ipv6_subnet = None + + # Store host user for permissions use + self._host_user = util.get_host_user() + self._certs = [] self.load_certs() @@ -107,9 +168,13 @@ def __init__(self, root_dir): self._timezone = tz[0] LOGGER.debug(f'System timezone is {self._timezone}') + # MQTT client + self._mqtt_client = mqtt.MQTT() + def start(self): self.reset() - self._status = 'Waiting for Device' + self._status = TestrunStatus.STARTING + self.pause_message = False self._started = datetime.datetime.now() def get_started(self): @@ -119,14 +184,14 @@ def get_finished(self): return self._finished def stop(self): - self.set_status('Stopping') + self.set_status(TestrunStatus.STOPPING) self.finish() def finish(self): # Set any in progress test results to Error for test_result in self._results: - if test_result.result == 'In Progress': - test_result.result = 'Error' + if test_result.result == TestResult.IN_PROGRESS: + test_result.result = TestResult.ERROR self._finished = datetime.datetime.now() @@ -139,9 +204,12 @@ def _get_default_config(self): 'log_level': 'INFO', 'startup_timeout': 60, 'monitor_period': 30, + 'allow_disconnect': False, 'max_device_reports': 0, 'api_url': 'http://localhost', - 'api_port': 8000 + 'api_port': 8000, + 'org_name': '', + 'single_intf': False, } def get_config(self): @@ -161,7 +229,7 @@ def _load_config(self): # Network interfaces if (NETWORK_KEY in config_file_json and DEVICE_INTF_KEY in config_file_json.get(NETWORK_KEY) - and INTERNET_INTF_KEY in config_file_json.get(NETWORK_KEY)): + and INTERNET_INTF_KEY in config_file_json.get(NETWORK_KEY)): self._config[NETWORK_KEY][DEVICE_INTF_KEY] = config_file_json.get( NETWORK_KEY, {}).get(DEVICE_INTF_KEY) self._config[NETWORK_KEY][INTERNET_INTF_KEY] = config_file_json.get( @@ -175,6 +243,10 @@ def _load_config(self): self._config[MONITOR_PERIOD_KEY] = config_file_json.get( MONITOR_PERIOD_KEY) + if ALLOW_DISCONNECT_KEY in config_file_json: + self._config[ALLOW_DISCONNECT_KEY] = config_file_json.get( + ALLOW_DISCONNECT_KEY) + if LOG_LEVEL_KEY in config_file_json: self._config[LOG_LEVEL_KEY] = config_file_json.get(LOG_LEVEL_KEY) @@ -188,13 +260,21 @@ def _load_config(self): self._config[MAX_DEVICE_REPORTS_KEY] = config_file_json.get( MAX_DEVICE_REPORTS_KEY) - LOGGER.debug(self._config) + if ORG_NAME_KEY in config_file_json: + self._config[ORG_NAME_KEY] = config_file_json.get( + ORG_NAME_KEY + ) + + if TEST_CONFIG_KEY in config_file_json: + self._config[TEST_CONFIG_KEY] = config_file_json.get( + TEST_CONFIG_KEY + ) def _load_version(self): version_cmd = util.run_command( 'dpkg-query --showformat=\'${Version}\' --show testrun') # index 1 of response is the stderr byte stream so if - # it has any data in it, there was an error and we + # it has any data in it, there was an error and wen # did not resolve the version and we'll use the fallback if len(version_cmd[1]) == 0: version = version_cmd[0] @@ -202,14 +282,37 @@ def _load_version(self): else: LOGGER.debug('Failed getting the version from dpkg-query') # Try getting the version from the make control file + + # Check if MAKE_CONTROL_DIR exists + if not os.path.exists(MAKE_CONTROL_DIR): + LOGGER.error('make/DEBIAN/control file path not found') + self._version = 'Unknown' + return + try: - version = util.run_command( - '$(grep -R "Version: " $MAKE_CONTROL_DIR | awk "{print $2}"') - except Exception as e: + # Run the grep command to find the version line + grep_cmd = util.run_command(f'grep -R "Version: " {MAKE_CONTROL_DIR}') + + if grep_cmd[0] and len(grep_cmd[1]) == 0: + # Extract the version number from grep + version = grep_cmd[0].split()[1] + self._version = version + LOGGER.debug(f'Testrun version is: {self._version}') + + else: + # Error handling if grep can't find the version line + self._version = 'Unknown' + LOGGER.debug(f'Testrun version is {self._version}') + raise Exception('Version line not found in make control file') + + except Exception as e: # pylint: disable=W0703 LOGGER.debug('Failed getting the version from make control file') LOGGER.error(e) self._version = 'Unknown' + def get_host_user(self): + return self._host_user + def get_version(self): return self._version @@ -225,11 +328,17 @@ def get_runtime_params(self): return self._runtime_params def add_runtime_param(self, param): + if param == 'single_intf': + self._config['single_intf'] = True self._runtime_params.append(param) def get_device_interface(self): return self._config.get(NETWORK_KEY, {}).get(DEVICE_INTF_KEY) + def get_device_interface_mac_addr(self): + iface = self.get_device_interface() + return IPControl.get_iface_mac_address(iface=iface) + def get_internet_interface(self): return self._config.get(NETWORK_KEY, {}).get(INTERNET_INTF_KEY) @@ -253,7 +362,7 @@ def set_config(self, config_json): self._save_config() # Update log level - LOGGER.debug(f'Setting log level to {config_json["log_level"]}') + LOGGER.debug(f'Setting log level to {config_json["log_level"]}') # pylint: disable=W1405 logger.set_log_level(config_json['log_level']) def set_target_device(self, device): @@ -291,18 +400,36 @@ def get_device(self, mac_addr): def remove_device(self, device): self._device_repository.remove(device) - def get_status(self): + def get_ipv4_subnet(self): + return self._ipv4_subnet + + def get_ipv6_subnet(self): + return self._ipv6_subnet + + def get_status(self) -> TestrunStatus: return self._status - def set_status(self, status): + def set_status(self, status: TestrunStatus): self._status = status + def get_result(self) -> TestrunResult: + return self._result + + def set_result(self, result: TestrunResult): + self._result = result + + def set_description(self, desc: str): + self._description = desc + def get_test_results(self): return self._results def get_module_reports(self): return self._module_reports + def get_module_templates(self): + return self._module_templates + def get_report_tests(self): """Returns the current test results in JSON-friendly format (in Python dictionary)""" @@ -322,19 +449,59 @@ def add_test_result(self, result): # result type is TestCase object if test_result.name == result.name: - # Just update the result and description - test_result.result = result.result - test_result.description = result.description - test_result.recommendations = result.recommendations + # Just update the result, description and recommendations + if len(result.description) != 0: + test_result.description = result.description + + # Add recommendations if provided + if result.recommendations is not None: + test_result.recommendations = result.recommendations + + if len(result.recommendations) == 0: + test_result.recommendations = None + + if result.result is not None: + + # Any informational test should always report informational + if test_result.required_result == 'Informational': + + # Set test result to informational + if result.result in [ + TestResult.NON_COMPLIANT, + TestResult.COMPLIANT, + TestResult.INFORMATIONAL + ]: + test_result.result = TestResult.INFORMATIONAL + else: + test_result.result = result.result + + # Copy any test recommendations to optional + test_result.optional_recommendations = result.recommendations + + # Remove recommendations from informational tests + test_result.recommendations = None + else: + test_result.result = result.result + updated = True if not updated: - result.result = 'In Progress' self._results.append(result) + def set_test_result_error(self, result, description=None): + """Set test result error""" + result.result = TestResult.ERROR + result.recommendations = None + if description is not None: + result.description = description + self._results.append(result) + def add_module_report(self, module_report): self._module_reports.append(module_report) + def add_module_template(self, module_template): + self._module_templates.append(module_template) + def get_all_reports(self): reports = [] @@ -348,6 +515,10 @@ def get_all_reports(self): def add_total_tests(self, no_tests): self._total_tests += no_tests + + def get_allow_disconnect(self): + return self._config.get(ALLOW_DISCONNECT_KEY) + def get_total_tests(self): return self._total_tests @@ -357,15 +528,24 @@ def get_report_url(self): def set_report_url(self, url): self._report_url = url + def get_export_url(self): + return self._export_url + + def set_export_url(self, url): + self._export_url = url + + def set_subnets(self, ipv4_subnet, ipv6_subnet): + self._ipv4_subnet = ipv4_subnet + self._ipv6_subnet = ipv6_subnet + def _load_profiles(self): # Load format of questionnaire LOGGER.debug('Loading risk assessment format') try: - with open(os.path.join( - self._root_dir, PROFILE_FORMAT_PATH - ), encoding='utf-8') as profile_format_file: + with open(os.path.join(self._root_dir, PROFILE_FORMAT_PATH), + encoding='utf-8') as profile_format_file: format_json = json.load(profile_format_file) # Save original profile format for internal validation self._profile_format = format_json @@ -374,6 +554,10 @@ def _load_profiles(self): 'An error occurred whilst loading the risk assessment format') LOGGER.debug(e) + # If the format JSON fails to load, skip loading profiles + LOGGER.error('Profiles will not be loaded') + return + profile_format_array = [] # Remove internal properties @@ -398,21 +582,42 @@ def _load_profiles(self): try: for risk_profile_file in os.listdir( - os.path.join(self._root_dir, PROFILES_DIR)): + os.path.join(self._root_dir, PROFILES_DIR)): + + if not risk_profile_file.endswith('.json'): + continue + LOGGER.debug(f'Discovered profile {risk_profile_file}') + # Open the risk profile file with open(os.path.join(self._root_dir, PROFILES_DIR, risk_profile_file), encoding='utf-8') as f: - json_data = json.load(f) - risk_profile = RiskProfile() - risk_profile.load( - profile_json=json_data, - profile_format=self._profile_format - ) - risk_profile.status = self.check_profile_status(risk_profile) + + # Parse risk profile json + json_data: dict = json.load(f) + + # Validate profile JSON + if not self.validate_profile_json(json_data): + LOGGER.error('Profile failed validation') + continue + + # Instantiate a new risk profile + risk_profile: RiskProfile = RiskProfile() + + # Assign the profile questions + questions: list[dict] = json_data.get('questions') + + # Pass only the valid questions to the risk profile + json_data['questions'] = self._remove_invalid_questions(questions) + + # Pass JSON to populate risk profile + risk_profile.load(profile_json=json_data, + profile_format=self._profile_format) + + # Add risk profile to session self._profiles.append(risk_profile) - except Exception as e: + except Exception as e: # pylint: disable=W0703 LOGGER.error('An error occurred whilst loading risk profiles') LOGGER.debug(e) @@ -428,25 +633,6 @@ def get_profile(self, name): return profile return None - def validate_profile(self, profile_json): - - # Check name field is present - if 'name' not in profile_json: - return False - - # Check questions field is present - if 'questions' not in profile_json: - return False - - # Check all questions are present - for format_q in self.get_profiles_format(): - if self._get_profile_question(profile_json, - format_q.get('question')) is None: - LOGGER.error('Missing question: ' + format_q.get('question')) - return False - - return True - def _get_profile_question(self, profile_json, question): for q in profile_json.get('questions'): @@ -455,7 +641,14 @@ def _get_profile_question(self, profile_json, question): return None + def get_profile_format_question(self, question): + for q in self.get_profiles_format(): + if q.get('question') == question: + return q + def update_profile(self, profile_json): + """Update the risk profile with the provided JSON. + The content has already been validated in the API""" profile_name = profile_json['name'] @@ -463,45 +656,19 @@ def update_profile(self, profile_json): profile_json['version'] = self.get_version() profile_json['created'] = datetime.datetime.now().strftime('%Y-%m-%d') - if 'status' in profile_json and profile_json.get('status') == 'Valid': - # Attempting to submit a risk profile, we need to check it - - # Check all questions have been answered - all_questions_answered = True - - for question in self.get_profiles_format(): + # Assign the profile questions + questions: list[dict] = profile_json.get('questions') - # Check question is present - profile_question = self._get_profile_question(profile_json, - question.get('question')) - - if profile_question is not None: - - # Check answer is present - if 'answer' not in profile_question: - LOGGER.error('Missing answer for question: ' + - question.get('question')) - all_questions_answered = False - - else: - LOGGER.error('Missing question: ' + question.get('question')) - all_questions_answered = False - - if not all_questions_answered: - LOGGER.error('Not all questions answered') - return None - - else: - profile_json['status'] = 'Draft' + # Pass only the valid questions to the risk profile + profile_json['questions'] = self._remove_invalid_questions(questions) + # Check if profile already exists risk_profile = self.get_profile(profile_name) - if risk_profile is None: # Create a new risk profile - risk_profile = RiskProfile( - profile_json=profile_json, - profile_format=self._profile_format) + risk_profile = RiskProfile(profile_json=profile_json, + profile_format=self._profile_format) self._profiles.append(risk_profile) else: @@ -524,19 +691,137 @@ def update_profile(self, profile_json): return risk_profile - def check_profile_status(self, profile): + def _remove_invalid_questions(self, questions): + """Remove unrecognised questions from the profile""" + + # Store valid questions + valid_questions = [] + + # Remove any additional (outdated questions from the profile) + for question in questions: + + # Check if question exists in the profile format + if self.get_profile_format_question( + question=question['question']) is not None: + + # Add the question to the valid_questions + valid_questions.append(question) + + else: + LOGGER.debug(f'Removed unrecognised question: {question["question"]}') # pylint: disable=W1405 + + # Return the list of valid questions + return valid_questions + + def validate_profile_json(self, profile_json): + """Validate properties in profile update requests""" + + # Get the status field + valid = False + if 'status' in profile_json and profile_json.get('status') == 'Valid': + valid = True + + # Check if 'name' exists in profile + if 'name' not in profile_json: + LOGGER.error('Missing "name" in profile') + return False + + # Check if 'name' field not empty + elif len(profile_json.get('name').strip()) == 0: + LOGGER.error('Name field left empty') + return False + + # Check if profile name has special characters + for field in ['name', 'rename']: + profile_name = profile_json.get(field) + if profile_name: + for char in profile_name: + if char in r"\<>?/:;@''][=^": + LOGGER.error('Profile name should not contain special characters') + return False + + # Error handling if 'questions' not in request + if 'questions' not in profile_json and valid: + LOGGER.error('Missing "questions" field in profile') + return False + + # Validating the questions section + for question in profile_json.get('questions'): + + # Check if the question field is present + if 'question' not in question: + LOGGER.error('The "question" field is missing') + return False + + # Check if 'question' field not empty + elif len(question.get('question').strip()) == 0: + LOGGER.error('A question is missing from "question" field') + return False + + # Check if question is a recognised question + format_q = self.get_profile_format_question( + question.get('question')) + + if format_q is None: + LOGGER.error(f'Unrecognised question: {question.get("question")}') # pylint: disable=W1405 + # Just ignore additional questions + continue + + # Error handling if 'answer' is missing + if 'answer' not in question and valid: + LOGGER.error('The answer field is missing') + return False + + # If answer is present, check the validation rules + else: + + # Extract the answer out of the profile + answer = question.get('answer') + + # Get the validation rules + field_type = format_q.get('type') - if profile.status == 'Valid': + # Check if type is string or single select, answer should be a string + if ((field_type in ['string', 'select']) + and not isinstance(answer, str)): + LOGGER.error(f'''Answer for question \ +{question.get('question')} is incorrect data type''') + return False - # Check expiry - created_date = profile.created.timestamp() + # Check if type is select, answer must be from list + if field_type == 'select' and valid: + possible_answers = format_q.get('options') + if answer not in possible_answers: + LOGGER.error(f'''Answer for question \ +{question.get('question')} is not valid''') + return False - today = datetime.datetime.now().timestamp() + # Validate select multiple field types + if field_type == 'select-multiple': - if created_date < (today - SECONDS_IN_YEAR): - profile.status = 'Expired' + if not isinstance(answer, list): + LOGGER.error(f'''Answer for question \ +{question.get('question')} is incorrect data type''') + return False - return profile.status + question_options_len = len(format_q.get('options')) + + # We know it is a list, now check the indexes + for index in answer: + + # Check if the index is an integer + if not isinstance(index, int): + LOGGER.error(f'''Answer for question \ +{question.get('question')} is incorrect data type''') + return False + + # Check if index is 0 or above and less than the num of options + if index < 0 or index >= question_options_len: + LOGGER.error(f'''Invalid index provided as answer for \ +question {question.get('question')}''') + return False + + return True def delete_profile(self, profile): @@ -551,20 +836,25 @@ def delete_profile(self, profile): return True - except Exception as e: + except Exception as e: # pylint: disable=W0703 LOGGER.error('An error occurred whilst deleting a profile') LOGGER.debug(e) return False def reset(self): - self.set_status('Idle') + self.set_status(TestrunStatus.IDLE) + self.pause_message = False + self.set_result(None) + self.set_description(None) self.set_target_device(None) self._report_url = None self._total_tests = 0 self._module_reports = [] + self._module_templates = [] self._results = [] self._started = None self._finished = None + self._ifaces = IPControl.get_sys_interfaces() def to_json(self): @@ -586,8 +876,15 @@ def to_json(self): 'tests': results } + if self.get_result() is not None: + session_json['result'] = self.get_result() + if self._report_url is not None: session_json['report'] = self.get_report_url() + if self._export_url is not None: + session_json['export'] = self.get_export_url() + + session_json['description'] = self._description return session_json @@ -601,17 +898,30 @@ def upload_cert(self, filename, content): # Parse bytes into x509 object cert = x509.load_pem_x509_certificate(content, default_backend()) - # Extract required properties - common_name = cert.subject.get_attributes_for_oid( - NameOID.COMMON_NAME)[0].value + # Retrieve the common name attributes from the subject + common_name_attr = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) + + # Raise an error if the common name attribute is missing + if not common_name_attr: + raise ValueError('Certificate is missing the common name') + + # Extract the organization name value + common_name = common_name_attr[0].value # Check if any existing certificates have the same common name for cur_cert in self._certs: if common_name == cur_cert['name']: raise ValueError('A certificate with that name already exists') - issuer = cert.issuer.get_attributes_for_oid( - NameOID.ORGANIZATION_NAME)[0].value + # Retrieve the organization name attributes from issuer + issuer_attr = cert.issuer.get_attributes_for_oid(NameOID.ORGANIZATION_NAME) + + # Raise an error if the organization name attribute is missing + if not issuer_attr: + raise ValueError('Certificate is missing the organization name') + + # Extract the organization name value + issuer = issuer_attr[0].value status = 'Valid' if now > cert.not_valid_after_utc: @@ -650,6 +960,11 @@ def load_certs(self): self._certs = [] for cert_file in os.listdir(CERTS_PATH): + + # Ignore directories + if os.path.isdir(os.path.join(CERTS_PATH, cert_file)): + continue + LOGGER.debug(f'Loading certificate {cert_file}') try: @@ -685,7 +1000,7 @@ def load_certs(self): self._certs.append(cert_obj) LOGGER.debug(f'Successfully loaded {cert_file}') - except Exception as e: + except Exception as e: # pylint: disable=W0703 LOGGER.error(f'An error occurred whilst loading {cert_file}') LOGGER.debug(e) @@ -705,10 +1020,32 @@ def delete_cert(self, filename): self._certs.remove(cert) return True - except Exception as e: + except Exception as e: # pylint: disable=W0703 LOGGER.error('An error occurred whilst deleting the certificate') LOGGER.debug(e) return False def get_certs(self): return self._certs + + def detect_network_adapters_change(self) -> dict: + adapters = {} + ifaces_new = IPControl.get_sys_interfaces() + + # Difference between stored and newly received network interfaces + diff = util.diff_dicts(self._ifaces, ifaces_new) + if diff: + if 'items_added' in diff: + adapters['adapters_added'] = diff['items_added'] + if 'items_removed' in diff: + adapters['adapters_removed'] = diff['items_removed'] + # Save new network interfaces to session + LOGGER.debug(f'Network adapters change detected: {adapters}') + self._ifaces = ifaces_new + return adapters + + def get_mqtt_client(self): + return self._mqtt_client + + def get_ifaces(self): + return self._ifaces diff --git a/framework/python/src/core/tasks.py b/framework/python/src/core/tasks.py new file mode 100644 index 000000000..5da0b40c9 --- /dev/null +++ b/framework/python/src/core/tasks.py @@ -0,0 +1,78 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Periodic background tasks""" + +from contextlib import asynccontextmanager +import datetime +import logging + +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from fastapi import FastAPI + +from common import logger + +# Check adapters period seconds +# Check adapters period seconds +CHECK_NETWORK_ADAPTERS_PERIOD = 5 +CHECK_INTERNET_PERIOD = 2 +INTERNET_CONNECTION_TOPIC = 'events/internet' +NETWORK_ADAPTERS_TOPIC = 'events/adapter' + +LOGGER = logger.get_logger('tasks') + + +class PeriodicTasks: + """Background periodic tasks + """ + def __init__( + self, testrun_obj, + ) -> None: + self._testrun = testrun_obj + self._mqtt_client = self._testrun.get_mqtt_client() + local_tz = datetime.datetime.now().astimezone().tzinfo + self._scheduler = AsyncIOScheduler(timezone=local_tz) + # Prevent scheduler warnings + self._scheduler._logger.setLevel(logging.ERROR) + + self.adapters_checker_job = self._scheduler.add_job( + func=self._testrun.get_net_orc().network_adapters_checker, + kwargs={ + 'mqtt_client': self._mqtt_client, + 'topic': NETWORK_ADAPTERS_TOPIC + }, + trigger='interval', + seconds=CHECK_NETWORK_ADAPTERS_PERIOD, + ) + # add internet connection cheking job only in single-intf mode + if 'single_intf' not in self._testrun.get_session().get_runtime_params(): + self.internet_shecker = self._scheduler.add_job( + func=self._testrun.get_net_orc().internet_conn_checker, + kwargs={ + 'mqtt_client': self._mqtt_client, + 'topic': INTERNET_CONNECTION_TOPIC + }, + trigger='interval', + seconds=CHECK_INTERNET_PERIOD, + ) + + @asynccontextmanager + async def start(self, app: FastAPI): # pylint: disable=unused-argument + """Start background tasks + + Args: + app (FastAPI): app instance + """ + # Job that checks for changes in network adapters + self._scheduler.start() + yield diff --git a/framework/python/src/core/test_runner.py b/framework/python/src/core/test_runner.py index 870e97752..b15e5e899 100644 --- a/framework/python/src/core/test_runner.py +++ b/framework/python/src/core/test_runner.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Wrapper for the Testrun that simplifies virtual testing procedure by allowing direct calling from the command line. @@ -20,11 +19,20 @@ E.g sudo cmd/start """ +# Disable warning about TripleDES being removed from cryptography in 48.0.0 +# Scapy 2.5.0 uses TripleDES +# Scapy 2.6.0 causes a bug in testrun when the device intf is being restarted +import warnings +from cryptography.utils import CryptographyDeprecationWarning +warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning) + +# pylint: disable=wrong-import-position import argparse import sys from testrun import Testrun from common import logger import signal +import io LOGGER = logger.get_logger("runner") @@ -37,13 +45,17 @@ def __init__(self, validate=False, net_only=False, single_intf=False, - no_ui=False): + no_ui=False, + target=None, + firmware=None): self._register_exits() - self.test_run = Testrun(config_file=config_file, + self._testrun = Testrun(config_file=config_file, validate=validate, net_only=net_only, single_intf=single_intf, - no_ui=no_ui) + no_ui=no_ui, + target_mac=target, + firmware=firmware) def _register_exits(self): signal.signal(signal.SIGINT, self._exit_handler) @@ -62,7 +74,7 @@ def _exit_handler(self, signum, arg): # pylint: disable=unused-argument sys.exit(1) def stop(self): - self.test_run.stop() + self._testrun.stop() def parse_args(): @@ -73,8 +85,7 @@ def parse_args(): "-f", "--config-file", default=None, - help="Define the configuration file for Testrun and Network Orchestrator" - ) + help="Define the configuration file for Testrun and Network Orchestrator") parser.add_argument( "--validate", default=False, @@ -91,7 +102,38 @@ def parse_args(): default=False, action="store_true", help="Do not launch the user interface") + parser.add_argument("--target", + default=None, + type=str, + help="MAC address of the target device") + parser.add_argument("-fw", + "--firmware", + default=None, + type=str, + help="Firmware version to be tested") + parsed_args = parser.parse_known_args()[0] + + if (parsed_args.no_ui and not parsed_args.net_only + and (parsed_args.target is None or parsed_args.firmware is None)): + # Capture help text + help_text = io.StringIO() + parser.print_help(file=help_text) + + # Get help text as lines and find where "Testrun" starts (skip usage) + help_lines = help_text.getvalue().splitlines() + start_index = next( + (i for i, line in enumerate(help_lines) if "Testrun" in line), 0) + + # Join only lines starting from "Testrun" and print without extra newlines + help_message = "\n".join(line.rstrip() for line in help_lines[start_index:]) + print(help_message) + + print( + "Error: --target and --firmware are required when --no-ui is specified", + file=sys.stderr) + sys.exit(1) + return parsed_args @@ -101,4 +143,6 @@ def parse_args(): validate=args.validate, net_only=args.net_only, single_intf=args.single_intf, - no_ui=args.no_ui) + no_ui=args.no_ui, + target=args.target, + firmware=args.firmware) diff --git a/framework/python/src/core/testrun.py b/framework/python/src/core/testrun.py index 5b43cfd65..4bc8b20c5 100644 --- a/framework/python/src/core/testrun.py +++ b/framework/python/src/core/testrun.py @@ -11,14 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -"""The overall control of the Test Run application. - +"""The overall control of the Testrun application. This file provides the integration between all of the -Test Run components, such as net_orc, test_orc and test_ui. - -Run using the provided command scripts in the cmd folder. -E.g sudo cmd/start +Testrun components, such as net_orc, test_orc and test_ui. """ import docker import json @@ -27,25 +22,19 @@ import signal import sys import time -from common import logger, util +import docker.errors + +from common import logger, util, mqtt from common.device import Device -from common.session import TestrunSession from common.testreport import TestReport +from common.statuses import TestrunStatus +from session import TestrunSession from api.api import Api from net_orc.listener import NetworkEvent from net_orc import network_orchestrator as net_orc from test_orc import test_orchestrator as test_orc -from docker.errors import ImageNotFound - -# Locate parent directory -current_dir = os.path.dirname(os.path.realpath(__file__)) - -# Locate the test-run root directory, 4 levels, src->python->framework->test-run -root_dir = os.path.dirname(os.path.dirname( - os.path.dirname(os.path.dirname(current_dir)))) - -LOGGER = logger.get_logger('test_run') +LOGGER = logger.get_logger('testrun') DEFAULT_CONFIG_FILE = 'local/system.json' EXAMPLE_CONFIG_FILE = 'local/system.json.example' @@ -58,10 +47,16 @@ DEVICE_MODEL = 'model' DEVICE_MAC_ADDR = 'mac_addr' DEVICE_TEST_MODULES = 'test_modules' +DEVICE_TYPE_KEY = 'type' +DEVICE_TECHNOLOGY_KEY = 'technology' +DEVICE_TEST_PACK_KEY = 'test_pack' +DEVICE_ADDITIONAL_INFO_KEY = 'additional_info' + MAX_DEVICE_REPORTS_KEY = 'max_device_reports' + class Testrun: # pylint: disable=too-few-public-methods - """Test Run controller. + """Testrun controller. Creates an instance of the network orchestrator, test orchestrator and user interface. @@ -72,8 +67,19 @@ def __init__(self, validate=False, net_only=False, single_intf=False, - no_ui=False): + no_ui=False, + target_mac=None, + firmware=None): + + # Locate parent directory + current_dir = os.path.dirname(os.path.realpath(__file__)) + # Locate the test-run root directory, 4 levels, + # src->python->framework->test-run + self._root_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))) + + # Determine config file if config_file is None: self._config_file = self._get_config_abs(DEFAULT_CONFIG_FILE) else: @@ -81,13 +87,15 @@ def __init__(self, self._net_only = net_only self._single_intf = single_intf - self._no_ui = no_ui + # Network only option only works if UI is also + # disbled so need to set no_ui if net_only is selected + self._no_ui = no_ui or net_only # Catch any exit signals self._register_exits() # Create session - self._session = TestrunSession(root_dir=root_dir) + self._session = TestrunSession(root_dir=self._root_dir) # Register runtime parameters if single_intf: @@ -97,18 +105,35 @@ def __init__(self, if validate: self._session.add_runtime_param('validate') - self._net_orc = net_orc.NetworkOrchestrator( - session=self._session) - self._test_orc = test_orc.TestOrchestrator( - self._session, - self._net_orc) + self._net_orc = net_orc.NetworkOrchestrator(session=self._session) + self._test_orc = test_orc.TestOrchestrator(self._session, self._net_orc) # Load device repository self.load_all_devices() + # If no_ui selected and not network only mode, + # load the target device into the session + if self._no_ui and not net_only: + target_device = self._session.get_device(target_mac) + if target_device is not None: + target_device.firmware = firmware + self._session.set_target_device(target_device) + else: + print( + f'Target device specified does not exist in device registry: ' + f'{target_mac}', + file=sys.stderr) + sys.exit(1) + # Load test modules self._test_orc.start() + # Start websockets server + self.start_ws() + + # Init MQTT client + self._mqtt_client = mqtt.MQTT() + if self._no_ui: # Check Testrun is able to start @@ -131,6 +156,9 @@ def __init__(self, while True: time.sleep(1) + def get_root_dir(self): + return self._root_dir + def get_version(self): return self.get_session().get_version() @@ -150,25 +178,33 @@ def _load_devices(self, device_dir): for device_folder in os.listdir(device_dir): - device_config_file_path = os.path.join(device_dir, - device_folder, + device_config_file_path = os.path.join(device_dir, device_folder, DEVICE_CONFIG) # Check if device config file exists before loading if not os.path.exists(device_config_file_path): LOGGER.error('Device configuration file missing ' + - f'from device {device_folder}') + f'for device {device_folder}') continue # Open device config file with open(device_config_file_path, encoding='utf-8') as device_config_file: - device_config_json = json.load(device_config_file) + + try: + device_config_json = json.load(device_config_file) + except json.decoder.JSONDecodeError as e: + LOGGER.error('Invalid JSON found in ' + + f'device configuration {device_config_file_path}') + LOGGER.debug(e) + continue device_manufacturer = device_config_json.get(DEVICE_MANUFACTURER) device_model = device_config_json.get(DEVICE_MODEL) mac_addr = device_config_json.get(DEVICE_MAC_ADDR) test_modules = device_config_json.get(DEVICE_TEST_MODULES) + + # Load max device reports max_device_reports = None if 'max_device_reports' in device_config_json: max_device_reports = device_config_json.get(MAX_DEVICE_REPORTS_KEY) @@ -183,6 +219,25 @@ def _load_devices(self, device_dir): max_device_reports=max_device_reports, device_folder=device_folder) + # Load in the additional fields + if DEVICE_TYPE_KEY in device_config_json: + device.type = device_config_json.get(DEVICE_TYPE_KEY) + + if DEVICE_TECHNOLOGY_KEY in device_config_json: + device.technology = device_config_json.get(DEVICE_TECHNOLOGY_KEY) + + if DEVICE_TEST_PACK_KEY in device_config_json: + device.test_pack = device_config_json.get(DEVICE_TEST_PACK_KEY) + + if DEVICE_ADDITIONAL_INFO_KEY in device_config_json: + device.additional_info = device_config_json.get( + DEVICE_ADDITIONAL_INFO_KEY) + + if None in [device.type, device.technology, device.test_pack]: + LOGGER.warning( + 'Device is outdated and requires further configuration') + device.status = 'Invalid' + self._load_test_reports(device) # Add device to device repository @@ -192,41 +247,39 @@ def _load_devices(self, device_dir): def _load_test_reports(self, device): - LOGGER.debug(f'Loading test reports for device {device.model}') + LOGGER.debug('Loading test reports for device ' + + f'{device.manufacturer} {device.model}') # Remove the existing reports in memory device.clear_reports() # Locate reports folder - reports_folder = os.path.join(root_dir, - LOCAL_DEVICES_DIR, - device.device_folder, 'reports') + reports_folder = self.get_reports_folder(device) # Check if reports folder exists (device may have no reports) if not os.path.exists(reports_folder): return - LOGGER.info(f'Loading reports from {reports_folder}') - for report_folder in os.listdir(reports_folder): # 1.3 file path - report_json_file_path = os.path.join( - reports_folder, - report_folder, - 'test', - device.mac_addr.replace(':',''), - 'report.json') - + report_json_file_path = os.path.join(reports_folder, report_folder, + 'test', + device.mac_addr.replace(':', ''), + 'report.json') + + if not os.path.isfile(report_json_file_path): + # Revert to pre 1.3 file path + report_json_file_path = os.path.join(reports_folder, report_folder, + 'report.json') + if not os.path.isfile(report_json_file_path): # Revert to pre 1.3 file path - report_json_file_path = os.path.join( - reports_folder, - report_folder, - 'report.json') + report_json_file_path = os.path.join(reports_folder, report_folder, + 'report.json') # Check if the report.json file exists if not os.path.isfile(report_json_file_path): - # Some error may have occured during this test run + # Some error may have occurred during this test run continue with open(report_json_file_path, encoding='utf-8') as report_json_file: @@ -236,18 +289,17 @@ def _load_test_reports(self, device): test_report.set_mac_addr(device.mac_addr) device.add_report(test_report) + def get_reports_folder(self, device): + """Return the reports folder path for the device""" + return os.path.join(self._root_dir, LOCAL_DEVICES_DIR, device.device_folder, + 'reports') + def delete_report(self, device: Device, timestamp): LOGGER.debug(f'Deleting test report for device {device.model} ' + f'at {timestamp}') # Locate reports folder - reports_folder = os.path.join(root_dir, - LOCAL_DEVICES_DIR, - device.device_folder, 'reports') - - # Check if reports folder exists (device may have no reports) - if not os.path.exists(reports_folder): - return False + reports_folder = self.get_reports_folder(device) for report_folder in os.listdir(reports_folder): if report_folder == timestamp: @@ -261,15 +313,13 @@ def delete_report(self, device: Device, timestamp): def create_device(self, device: Device): # Define the device folder location - device_folder_path = os.path.join(root_dir, - LOCAL_DEVICES_DIR, + device_folder_path = os.path.join(self._root_dir, LOCAL_DEVICES_DIR, device.device_folder) # Create the directory os.makedirs(device_folder_path) - config_file_path = os.path.join(device_folder_path, - DEVICE_CONFIG) + config_file_path = os.path.join(device_folder_path, DEVICE_CONFIG) with open(config_file_path, 'w', encoding='utf-8') as config_file: config_file.writelines(json.dumps(device.to_config_json(), indent=4)) @@ -282,23 +332,12 @@ def create_device(self, device: Device): return device.to_config_json() - def save_device(self, device: Device, device_json): + def save_device(self, device: Device): """Edit and save an existing device config.""" - # Update device properties - device.manufacturer = device_json['manufacturer'] - device.model = device_json['model'] - - if 'test_modules' in device_json: - device.test_modules = device_json['test_modules'] - else: - device.test_modules = {} - # Obtain the config file path - config_file_path = os.path.join(root_dir, - LOCAL_DEVICES_DIR, - device.device_folder, - DEVICE_CONFIG) + config_file_path = os.path.join(self._root_dir, LOCAL_DEVICES_DIR, + device.device_folder, DEVICE_CONFIG) with open(config_file_path, 'w+', encoding='utf-8') as config_file: config_file.writelines(json.dumps(device.to_config_json(), indent=4)) @@ -311,9 +350,8 @@ def save_device(self, device: Device, device_json): def delete_device(self, device: Device): # Obtain the config file path - device_folder = os.path.join(root_dir, - LOCAL_DEVICES_DIR, - device.device_folder) + device_folder = os.path.join(self._root_dir, LOCAL_DEVICES_DIR, + device.device_folder) # Delete the device directory shutil.rmtree(device_folder) @@ -328,19 +366,16 @@ def start(self): self._start_network() self.get_net_orc().get_listener().register_callback( - self._device_discovered, - [NetworkEvent.DEVICE_DISCOVERED] - ) + self._device_discovered, [NetworkEvent.DEVICE_DISCOVERED]) if self._net_only: LOGGER.info('Network only option configured, no tests will be run') else: self.get_net_orc().get_listener().register_callback( - self._device_stable, - [NetworkEvent.DEVICE_STABLE] - ) + self._device_stable, [NetworkEvent.DEVICE_STABLE]) self.get_net_orc().start_listener() + self.get_session().set_status(TestrunStatus.WAITING_FOR_DEVICE) LOGGER.info('Waiting for devices on the network...') # Keep application running until stopped @@ -349,15 +384,21 @@ def start(self): def stop(self): + # First, change the status to stopping + self.get_session().stop() + # Prevent discovering new devices whilst stopping if self.get_net_orc().get_listener() is not None: self.get_net_orc().get_listener().stop_listener() - self.get_session().stop() - self._stop_tests() + + self.get_session().set_status(TestrunStatus.CANCELLED) + + # Disconnect before WS server stops to prevent error + self._mqtt_client.disconnect() + self._stop_network(kill=True) - self.get_session().set_status('Cancelled') def _register_exits(self): signal.signal(signal.SIGINT, self._exit_handler) @@ -369,6 +410,7 @@ def shutdown(self): LOGGER.info('Shutting down Testrun') self.stop() self._stop_ui() + self._stop_ws() def _exit_handler(self, signum, arg): # pylint: disable=unused-argument LOGGER.debug('Exit signal received: ' + str(signum)) @@ -380,7 +422,7 @@ def _exit_handler(self, signum, arg): # pylint: disable=unused-argument def _get_config_abs(self, config_file=None): if config_file is None: # If not defined, use relative pathing to local file - config_file = os.path.join(root_dir, self._config_file) + config_file = os.path.join(self._root_dir, self._config_file) # Expand the config file to absolute pathing return os.path.abspath(config_file) @@ -406,6 +448,9 @@ def _stop_network(self, kill=True): def _stop_tests(self): self._test_orc.stop() + def get_mqtt_client(self): + return self._mqtt_client + def get_device(self, mac_addr): """Returns a loaded device object from the device mac address.""" for device in self.get_session().get_device_repository(): @@ -419,6 +464,8 @@ def _device_discovered(self, mac_addr): if device is not None: if mac_addr != device.mac_addr: + LOGGER.info(f'Found device with mac addr: {mac_addr} but was ignored') + LOGGER.info(f'Expected device mac address is {device.mac_addr}') # Ignore discovered device because it is not the target device return else: @@ -435,16 +482,14 @@ def _device_discovered(self, mac_addr): def _device_stable(self, mac_addr): # Do not continue testing if Testrun has cancelled during monitor phase - if self.get_session().get_status() == 'Cancelled': + if self.get_session().get_status() == TestrunStatus.CANCELLED: self._stop_network() return LOGGER.info(f'Device with mac address {mac_addr} is ready for testing.') - self._set_status('In Progress') - result = self._test_orc.run_test_modules() + self._set_status(TestrunStatus.IN_PROGRESS) + self._test_orc.run_test_modules() - if result is not None: - self._set_status(result) self._stop_network() def get_session(self): @@ -462,18 +507,14 @@ def start_ui(self): client = docker.from_env() try: - client.containers.run( - image='test-run/ui', - auto_remove=True, - name='tr-ui', - hostname='testrun.io', - detach=True, - ports={ - '80': 8080 - } - ) - except ImageNotFound as ie: - LOGGER.error('An error occured whilst starting the UI. ' + + client.containers.run(image='testrun/ui', + auto_remove=True, + name='tr-ui', + hostname='testrun.io', + detach=True, + ports={'80': 8080}) + except docker.errors.ImageNotFound as ie: + LOGGER.error('An error occurred whilst starting the UI. ' + 'Please investigate and try again.') LOGGER.error(ie) sys.exit(1) @@ -488,5 +529,49 @@ def _stop_ui(self): container = client.containers.get('tr-ui') if container is not None: container.kill() + # If the container has been started without auto-remove flag remove it + try: + container.remove() + except docker.errors.APIError: + pass except docker.errors.NotFound: - return + pass + + def start_ws(self): + + self._stop_ws() + + LOGGER.info('Starting WS server') + + client = docker.from_env() + + try: + client.containers.run(image='testrun/ws', + auto_remove=True, + name='tr-ws', + detach=True, + ports={ + '9001': 9001, + '1883': 1883 + }) + except docker.errors.ImageNotFound as ie: + LOGGER.error('An error occurred whilst starting the websockets server. ' + + 'Please investigate and try again.') + LOGGER.error(ie) + sys.exit(1) + + def _stop_ws(self): + LOGGER.info('Stopping websockets server') + client = docker.from_env() + try: + container = client.containers.get('tr-ws') + if container is not None: + container.kill() + # If the container has been started without auto-remove flag remove it + try: + container.remove() + except docker.errors.APIError: + pass + + except docker.errors.NotFound: + pass diff --git a/framework/python/src/net_orc/ip_control.py b/framework/python/src/net_orc/ip_control.py index 506b23a95..c6df5d91e 100644 --- a/framework/python/src/net_orc/ip_control.py +++ b/framework/python/src/net_orc/ip_control.py @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. """IP Control Module""" +import psutil +import typing as t from common import logger from common import util import re +import socket LOGGER = logger.get_logger('ip_ctrl') @@ -43,10 +46,7 @@ def add_namespace(self, namespace): def check_interface_status(self, interface_name): output = util.run_command(cmd=f'ip link show {interface_name}', output=True) - if 'state DOWN ' in output[0]: - return False - else: - return True + return 'state UP ' in output[0] def delete_link(self, interface_name): """Delete an ip link""" @@ -89,7 +89,17 @@ def get_iface_connection_stats(self, iface): else: return None - def get_iface_port_stats(self, iface): + @staticmethod + def get_iface_mac_address(iface): + net_if_addrs = psutil.net_if_addrs() + if iface in net_if_addrs: + for addr_info in net_if_addrs[iface]: + # AF_LINK corresponds to the MAC address + if addr_info.family == psutil.AF_LINK: + return addr_info.address + return None + + def get_iface_ethtool_port_stats(self, iface): """Extract information about packets connection""" response = util.run_command(f'ethtool -S {iface}') if len(response[1]) == 0: @@ -97,9 +107,25 @@ def get_iface_port_stats(self, iface): else: return None + def get_iface_ifconfig_port_stats(self, iface): + """Extract information about packets connection""" + response = util.run_command(f'ifconfig -a {iface}') + if len(response[1]) == 0: + return response[0] + else: + return None + + def get_ip_address(self, iface): + addrs = psutil.net_if_addrs() + if iface in addrs: + for addr in addrs[iface]: + if addr.family == socket.AF_INET: + return addr.address + return None + def get_namespaces(self): result = util.run_command('ip netns list') - #Strip ID's from the namespace results + # Strip ID's from the namespace results namespaces = re.findall(r'(\S+)(?:\s+\(id: \d+\))?', result[0]) return namespaces @@ -237,3 +263,30 @@ def configure_container_interface(self, LOGGER.error(f'Failed to set interface up {namespace_intf}') return False return True + + def ping_via_gateway(self, host): + """Ping the host trough the gateway container""" + command = f'timeout 3 docker exec tr-ct-gateway ping -W 1 -c 1 {host}' + output = util.run_command(command, supress_error=True) + if '0% packet loss' in output[0]: + return True + return False + + @staticmethod + def get_sys_interfaces() -> t.Dict[str, t.Dict[str, str]]: + """ Retrieves all Ethernet network interfaces from the host system + Returns: + t.Dict[str, str] + """ + addrs = psutil.net_if_addrs() + ifaces = {} + + for key in addrs: + nic = addrs[key] + # Ignore any interfaces that are not ethernet + if not (key.startswith('en') or key.startswith('eth')): + continue + + ifaces[key] = nic[0].address + + return ifaces diff --git a/framework/python/src/net_orc/listener.py b/framework/python/src/net_orc/listener.py index 03fcaaaf8..af79f1cf3 100644 --- a/framework/python/src/net_orc/listener.py +++ b/framework/python/src/net_orc/listener.py @@ -16,6 +16,7 @@ under test.""" import threading from scapy.all import AsyncSniffer, DHCP, get_if_hwaddr +from scapy.error import Scapy_Exception from net_orc.network_event import NetworkEvent from common import logger @@ -46,7 +47,7 @@ def start_listener(self): """Start sniffing packets on the device interface.""" # Don't start the listener if it is already running - if self._sniffer.running: + if self.is_running(): LOGGER.debug('Listener was already running') return @@ -58,8 +59,12 @@ def reset(self): def stop_listener(self): """Stop sniffing packets on the device interface.""" - if self._sniffer.running: - self._sniffer.stop() + try: + if self.is_running(): + self._sniffer.stop() + LOGGER.debug('Stopped the network listener') + except Scapy_Exception as e: + LOGGER.error(f'Error stopping the listener: {e}') def is_running(self): """Determine whether the sniffer is running.""" diff --git a/framework/python/src/net_orc/network_orchestrator.py b/framework/python/src/net_orc/network_orchestrator.py index f20093a28..ee340e0d2 100644 --- a/framework/python/src/net_orc/network_orchestrator.py +++ b/framework/python/src/net_orc/network_orchestrator.py @@ -17,18 +17,20 @@ import json import os from scapy.all import sniff, wrpcap, BOOTP, AsyncSniffer +from scapy.error import Scapy_Exception import shutil import subprocess import sys -import docker import time -from docker.types import Mount -from common import logger, util +import traceback +from common import logger, util, mqtt +from common.statuses import TestrunStatus from net_orc.listener import Listener from net_orc.network_event import NetworkEvent from net_orc.network_validator import NetworkValidator from net_orc.ovs_control import OVSControl from net_orc.ip_control import IPControl +from core.docker.network_docker_module import NetworkModule LOGGER = logger.get_logger('net_orc') RUNTIME_DIR = 'runtime' @@ -66,6 +68,10 @@ def __init__(self, session): self._ovs = OVSControl(self._session) self._ip_ctrl = IPControl() + # Load subnet information into the session + self._session.set_subnets(self.network_config.ipv4_network, + self.network_config.ipv6_network) + def start(self): """Start the network orchestrator.""" @@ -130,13 +136,22 @@ def start_network(self): self.create_net() self.start_network_services() - if 'validate' in self._session.get_runtime_params(): - # Start the validator after network is ready - self.validator.start() + try: + if 'validate' in self._session.get_runtime_params(): + # Start the validator after network is ready + self._session.set_status(TestrunStatus.VALIDATING) + self.validator.start() + self.validator.stop() + except Exception as e: # pylint: disable=W0703 + LOGGER.error(f'Validation failed {e}') + self._session.set_status('Waiting for Device') # Get network ready (via Network orchestrator) LOGGER.debug('Network is ready') + def get_ip_address(self, iface): + return self._ip_ctrl.get_ip_address(iface) + def get_listener(self): return self._listener @@ -190,7 +205,7 @@ def _device_discovered(self, mac_addr): test_dir = os.path.join(RUNTIME_DIR, TEST_DIR) device_tests = os.listdir(test_dir) for device_test in device_tests: - device_test_path = os.path.join(RUNTIME_DIR,TEST_DIR,device_test) + device_test_path = os.path.join(RUNTIME_DIR, TEST_DIR, device_test) if os.path.isdir(device_test_path): shutil.rmtree(device_test_path, ignore_errors=True) @@ -215,7 +230,7 @@ def _device_discovered(self, mac_addr): if device.ip_addr is None: LOGGER.info( f'Timed out whilst waiting for {mac_addr} to obtain an IP address') - self._session.set_status('Cancelled') + self._session.set_status(TestrunStatus.CANCELLED) return LOGGER.info( f'Device with mac addr {device.mac_addr} has obtained IP address ' @@ -223,7 +238,9 @@ def _device_discovered(self, mac_addr): #self._ovs.add_arp_inspection_filter(ip_address=device.ip_addr, # mac_address=device.mac_addr) - self._start_device_monitor(device) + # Don't monitor devices when in network only mode + if 'net_only' not in self._session.get_runtime_params(): + self._start_device_monitor(device) def _get_conn_stats(self): """ Extract information about the physical connection @@ -240,14 +257,21 @@ def _get_conn_stats(self): def _get_port_stats(self, pre_monitor=True): """ Extract information about the port statistics and store it to a file for the conn test module to access""" + suffix = 'pre_monitor' if pre_monitor else 'post_monitor' dev_int = self._session.get_device_interface() - port_stats = self._ip_ctrl.get_iface_port_stats(dev_int) - if port_stats is not None: - suffix = 'pre_monitor' if pre_monitor else 'post_monitor' - eth_out_file = os.path.join(NET_DIR, f'ethtool_port_stats_{suffix}.txt') - with open(eth_out_file, 'w', encoding='utf-8') as f: - f.write(port_stats) - else: + ethtool_port_stats = self._ip_ctrl.get_iface_ethtool_port_stats(dev_int) + ifconfig_port_stats = self._ip_ctrl.get_iface_ifconfig_port_stats(dev_int) + if ethtool_port_stats is not None: + ethtool_out_file = os.path.join(NET_DIR, + f'ethtool_port_stats_{suffix}.txt') + with open(ethtool_out_file, 'w', encoding='utf-8') as f: + f.write(ethtool_port_stats) + if ifconfig_port_stats is not None: + ifconfig_out_file = os.path.join(NET_DIR, + f'ifconfig_port_stats_{suffix}.txt') + with open(ifconfig_out_file, 'w', encoding='utf-8') as f: + f.write(ifconfig_port_stats) + if ethtool_port_stats is None and ifconfig_port_stats is None: LOGGER.error('Failed to generate port stats') def monitor_in_progress(self): @@ -273,7 +297,7 @@ def _dhcp_lease_ack(self, packet): def _start_device_monitor(self, device): """Start a timer until the steady state has been reached and callback the steady state method for this device.""" - self.get_session().set_status('Monitoring') + self.get_session().set_status(TestrunStatus.MONITORING) self._monitor_packets = [] LOGGER.info(f'Monitoring device with mac addr {device.mac_addr} ' f'for {str(self._session.get_monitor_period())} seconds') @@ -290,15 +314,20 @@ def _start_device_monitor(self, device): time.sleep(1) # Check Testrun hasn't been cancelled - if self._session.get_status() == 'Cancelled': + if self._session.get_status() in (TestrunStatus.STOPPING, + TestrunStatus.CANCELLED): sniffer.stop() return - - if not self._ip_ctrl.check_interface_status( - self._session.get_device_interface()): - sniffer.stop() - self._session.set_status('Cancelled') - LOGGER.error('Device interface disconnected, cancelling Testrun') + if not self._session.get_allow_disconnect(): + if not self._ip_ctrl.check_interface_status( + self._session.get_device_interface()): + try: + sniffer.stop() + except Scapy_Exception: + LOGGER.error('Device adapter disconnected whilst monitoring.') + finally: + self._session.set_status(TestrunStatus.CANCELLED) + LOGGER.error('Device interface disconnected, cancelling Testrun') LOGGER.debug('Writing packets to monitor.pcap') wrpcap(os.path.join(device_runtime_dir, 'monitor.pcap'), @@ -330,26 +359,6 @@ def _ping(self, net_module): success = util.run_command(cmd, output=False) return success - def _create_private_net(self): - client = docker.from_env() - try: - network = client.networks.get(PRIVATE_DOCKER_NET) - network.remove() - except docker.errors.NotFound: - pass - - # TODO: These should be made into variables - ipam_pool = docker.types.IPAMPool(subnet='100.100.0.0/16', - iprange='100.100.100.0/24') - - ipam_config = docker.types.IPAMConfig(pool_configs=[ipam_pool]) - - client.networks.create(PRIVATE_DOCKER_NET, - ipam=ipam_config, - internal=True, - check_duplicate=True, - driver='macvlan') - def _ci_pre_network_create(self): """ Stores network properties to restore network after network creation and flushes internet interface @@ -418,9 +427,7 @@ def create_net(self): # a use case is determined #self._create_private_net() - # Listener may have already been created. Only create if not - if self._listener is None: - self._listener = Listener(self._session) + self._listener = Listener(self._session) self.get_listener().register_callback(self._device_discovered, [NetworkEvent.DEVICE_DISCOVERED]) @@ -436,79 +443,33 @@ def load_network_modules(self): for module_dir in os.listdir(net_modules_dir): - if self._get_network_module(module_dir) is None: + if (self._get_network_module(module_dir) is None + and module_dir != 'template'): loaded_module = self._load_network_module(module_dir) loaded_modules += loaded_module.dir_name + ' ' LOGGER.info(loaded_modules) def _load_network_module(self, module_dir): + """Import module configuration from module_config.json.""" - net_modules_dir = os.path.join(self._path, NETWORK_MODULES_DIR) + # Make sure we only load each module once since some modules will + # depend on the same module + if not any(m.dir_name == module_dir for m in self._net_modules): + + LOGGER.debug(f'Loading network module {module_dir}') + + modules_dir = os.path.join(self._path, NETWORK_MODULES_DIR) + + module_conf_file = os.path.join(self._path, modules_dir, module_dir, + NETWORK_MODULE_METADATA) - net_module = NetworkModule() - - # Load module information - with open(os.path.join(self._path, net_modules_dir, module_dir, - NETWORK_MODULE_METADATA), - 'r', - encoding='UTF-8') as module_file_open: - net_module_json = json.load(module_file_open) - - net_module.name = net_module_json['config']['meta']['name'] - net_module.display_name = net_module_json['config']['meta']['display_name'] - net_module.description = net_module_json['config']['meta']['description'] - net_module.dir = os.path.join(self._path, net_modules_dir, module_dir) - net_module.dir_name = module_dir - net_module.build_file = module_dir + '.Dockerfile' - net_module.container_name = 'tr-ct-' + net_module.dir_name - net_module.image_name = 'test-run/' + net_module.dir_name - - # Attach folder mounts to network module - if 'docker' in net_module_json['config']: - - if 'mounts' in net_module_json['config']['docker']: - for mount_point in net_module_json['config']['docker']['mounts']: - net_module.mounts.append( - Mount(target=mount_point['target'], - source=os.path.join(os.getcwd(), mount_point['source']), - type='bind')) - - if 'depends_on' in net_module_json['config']['docker']: - depends_on_module = net_module_json['config']['docker']['depends_on'] - if self._get_network_module(depends_on_module) is None: - self._load_network_module(depends_on_module) - - # Determine if this is a container or just an image/template - if 'enable_container' in net_module_json['config']['docker']: - net_module.enable_container = net_module_json['config']['docker'][ - 'enable_container'] - - # Determine if this is a template - if 'template' in net_module_json['config']['docker']: - net_module.template = net_module_json['config']['docker']['template'] - - # Load network service networking configuration - if net_module.enable_container: - - net_module.net_config.enable_wan = net_module_json['config']['network'][ - 'enable_wan'] - net_module.net_config.ip_index = net_module_json['config']['network'][ - 'ip_index'] - - net_module.net_config.host = False if not 'host' in net_module_json[ - 'config']['network'] else net_module_json['config']['network']['host'] - - net_module.net_config.ipv4_address = self.network_config.ipv4_network[ - net_module.net_config.ip_index] - net_module.net_config.ipv4_network = self.network_config.ipv4_network - - net_module.net_config.ipv6_address = self.network_config.ipv6_network[ - net_module.net_config.ip_index] - net_module.net_config.ipv6_network = self.network_config.ipv6_network - - self._net_modules.append(net_module) - return net_module + module = NetworkModule(module_conf_file, self._session) + if module.depends_on is not None: + self._load_network_module(module.depends_on) + self._net_modules.append(module) + + return module def build_network_modules(self): LOGGER.info('Building network modules...') @@ -518,12 +479,7 @@ def build_network_modules(self): def _build_module(self, net_module): LOGGER.debug('Building network module ' + net_module.dir_name) - client = docker.from_env() - client.images.build(dockerfile=os.path.join(net_module.dir, - net_module.build_file), - path=self._path, - forcerm=True, - tag='test-run/' + net_module.dir_name) + net_module.build() def _get_network_module(self, name): for net_module in self._net_modules: @@ -535,65 +491,17 @@ def _get_network_module(self, name): def _start_network_service(self, net_module): LOGGER.debug('Starting network service ' + net_module.display_name) - network = 'host' if net_module.net_config.host else PRIVATE_DOCKER_NET + network = 'host' if net_module.net_config.host else 'bridge' LOGGER.debug(f"""Network: {network}, image name: {net_module.image_name}, container name: {net_module.container_name}""") - try: - client = docker.from_env() - net_module.container = client.containers.run( - net_module.image_name, - auto_remove=True, - cap_add=['NET_ADMIN'], - name=net_module.container_name, - hostname=net_module.container_name, - # Undetermined version of docker seems to have broken - # DNS configuration (/etc/resolv.conf) Re-add when/if - # this network is utilized and DNS issue is resolved - #network=PRIVATE_DOCKER_NET, - network_mode='none', - privileged=True, - detach=True, - mounts=net_module.mounts, - environment={ - 'TZ': self.get_session().get_timezone(), - 'HOST_USER': util.get_host_user() - }) - except docker.errors.ContainerError as error: - LOGGER.error('Container run error') - LOGGER.error(error) - - if network != 'host': + net_module.start() + if net_module.get_network() != 'host': self._attach_service_to_network(net_module) def _stop_service_module(self, net_module, kill=False): LOGGER.debug('Stopping network container ' + net_module.container_name) - try: - container = self._get_service_container(net_module) - if container is not None: - if kill: - LOGGER.debug('Killing container: ' + net_module.container_name) - container.kill() - else: - LOGGER.debug('Stopping container: ' + net_module.container_name) - container.stop() - LOGGER.debug('Container stopped: ' + net_module.container_name) - except Exception as error: # pylint: disable=W0703 - LOGGER.error('Container stop error') - LOGGER.error(error) - - def _get_service_container(self, net_module): - LOGGER.debug('Resolving service container: ' + net_module.container_name) - container = None - try: - client = docker.from_env() - container = client.containers.get(net_module.container_name) - except docker.errors.NotFound: - LOGGER.debug('Container ' + net_module.container_name + ' not found') - except Exception as e: # pylint: disable=W0703 - LOGGER.error('Failed to resolve container') - LOGGER.error(e) - return container + net_module.stop(kill=kill) def stop_networking_services(self, kill=False): LOGGER.info('Stopping network services') @@ -616,7 +524,10 @@ def start_network_services(self): if not net_module.enable_container: continue - self._start_network_service(net_module) + if net_module.enabled: + self._start_network_service(net_module) + else: + LOGGER.debug(f'Not starting disabled network module {net_module.name}') LOGGER.info('All network services are running') self._check_network_services() @@ -758,13 +669,10 @@ def restore_net(self): if self.get_listener() is not None and self.get_listener().is_running(): self.get_listener().stop_listener() - client = docker.from_env() - # Stop all network containers if still running for net_module in self._net_modules: try: - container = client.containers.get('tr-ct-' + net_module.dir_name) - container.kill() + net_module.stop(kill=True) except Exception: # pylint: disable=W0703 continue @@ -786,52 +694,50 @@ def restore_net(self): def get_session(self): return self._session + def network_adapters_checker(self, mqtt_client: mqtt.MQTT, topic: str): + """Checks for changes in network adapters + and sends a message to the frontend + """ + try: + adapters = self._session.detect_network_adapters_change() + if adapters: + mqtt_client.send_message(topic, adapters) + except Exception: # pylint: disable=W0703 + LOGGER.error(traceback.format_exc()) + + def is_device_connected(self): + """Check if device connected""" + return self._ip_ctrl.check_interface_status( + self._session.get_device_interface()) -class NetworkModule: - """Define all the properties of a Network Module""" - - def __init__(self): - self.name = None - self.display_name = None - self.description = None - - self.container = None - self.container_name = None - self.image_name = None - self.template = False - - # Absolute path - self.dir = None - self.dir_name = None - self.build_file = None - self.mounts = [] - - self.enable_container = True - - self.net_config = NetworkModuleNetConfig() - + def internet_conn_checker(self, mqtt_client: mqtt.MQTT, topic: str): + """Checks internet connection and sends a status to frontend""" -class NetworkModuleNetConfig: - """Define all the properties of the network config - for a network module""" + # Default message + message = {'connection': False} - def __init__(self): + # Only check if Testrun is running + if self.get_session().get_status() not in [ + TestrunStatus.WAITING_FOR_DEVICE, TestrunStatus.MONITORING, + TestrunStatus.IN_PROGRESS, TestrunStatus.STARTING + ]: + message['connection'] = None - self.enable_wan = False + # Only run if single intf mode not used + elif 'single_intf' not in self._session.get_runtime_params(): + iface = self._session.get_internet_interface() - self.ip_index = 0 - self.ipv4_address = None - self.ipv4_network = None - self.ipv6_address = None - self.ipv6_network = None + # Check that an internet intf has been selected + if iface and iface in self._ip_ctrl.get_sys_interfaces(): - self.host = False + # Ping google.com from gateway container + internet_connection = self._ip_ctrl.ping_via_gateway('google.com') - def get_ipv4_addr_with_prefix(self): - return format(self.ipv4_address) + '/' + str(self.ipv4_network.prefixlen) + if internet_connection: + message['connection'] = True - def get_ipv6_addr_with_prefix(self): - return format(self.ipv6_address) + '/' + str(self.ipv6_network.prefixlen) + # Broadcast via MQTT client + mqtt_client.send_message(topic, message) class NetworkConfig: diff --git a/framework/python/src/net_orc/network_validator.py b/framework/python/src/net_orc/network_validator.py index df9b96b1d..c50d2463d 100644 --- a/framework/python/src/net_orc/network_validator.py +++ b/framework/python/src/net_orc/network_validator.py @@ -22,6 +22,7 @@ import getpass from common import logger from common import util +from net_orc.ovs_control import OVSControl LOGGER = logger.get_logger('validator') OUTPUT_DIR = 'runtime/validation' @@ -46,6 +47,8 @@ def __init__(self): shutil.rmtree(os.path.join(self._path, OUTPUT_DIR), ignore_errors=True) + self._ovs = OVSControl(session=None) + def start(self): """Start the network validator.""" LOGGER.debug('Starting validator') @@ -87,6 +90,8 @@ def _build_device(self, net_device): def _load_devices(self): LOGGER.info(f'Loading validators from {self._device_dir}') + # Reset device list before loading + self._net_devices = [] loaded_devices = 'Loaded the following validators: ' @@ -106,7 +111,7 @@ def _load_devices(self): device.dir_name = module_dir device.build_file = module_dir + '.Dockerfile' device.container_name = 'tr-ct-' + device.dir_name - device.image_name = 'test-run/' + device.dir_name + device.image_name = 'testrun/' + device.dir_name runtime_source = os.path.join(os.getcwd(), OUTPUT_DIR, device.name) conf_source = os.path.join(os.getcwd(), CONF_DIR) @@ -286,7 +291,6 @@ def _stop_network_device(self, net_device, kill=False): LOGGER.error(e) def _get_device_container(self, net_device): - LOGGER.debug('Resolving device container: ' + net_device.container_name) container = None try: client = docker.from_env() @@ -305,6 +309,9 @@ def _stop_network_devices(self, kill=False): if not net_device.enable_container: continue self._stop_network_device(net_device, kill) + # Remove the device port form the ovs bridge once validation is done + bridge_intf = DEVICE_BRIDGE + 'i-' + net_device.dir_name + self._ovs.delete_port(DEVICE_BRIDGE,bridge_intf) class FauxDevice: # pylint: disable=too-few-public-methods,too-many-instance-attributes diff --git a/framework/python/src/net_orc/ovs_control.py b/framework/python/src/net_orc/ovs_control.py index 08faa52c1..2c1e17776 100644 --- a/framework/python/src/net_orc/ovs_control.py +++ b/framework/python/src/net_orc/ovs_control.py @@ -59,6 +59,14 @@ def delete_flow(self, bridge_name, flow): success = util.run_command(f'ovs-ofctl del-flows {bridge_name} \'{flow}\'') return success + def delete_port(self, bridge_name, port): + # Delete a port from the bridge using ovs-ofctl commands + success=True + if self.port_exists(bridge_name, port): + LOGGER.debug(f'Deleting port {port} from bridge: {bridge_name}') + success = util.run_command(f'ovs-vsctl del-port {bridge_name} \'{port}\'') + return success + def get_bridge_ports(self, bridge_name): # Get a list of all the ports on a bridge response = util.run_command(f'ovs-vsctl list-ports {bridge_name}', diff --git a/framework/python/src/test_orc/test_case.py b/framework/python/src/test_orc/test_case.py index cf0d6593a..6f4e3434b 100644 --- a/framework/python/src/test_orc/test_case.py +++ b/framework/python/src/test_orc/test_case.py @@ -14,6 +14,7 @@ """Represents an individual test case.""" from dataclasses import dataclass, field +from common.statuses import TestResult @dataclass @@ -24,25 +25,25 @@ class TestCase: # pylint: disable=too-few-public-methods,too-many-instance-attr description: str = "" expected_behavior: str = "" required_result: str = "Recommended" - result: str = "Non-Compliant" + result: str = TestResult.NON_COMPLIANT recommendations: list = field(default_factory=lambda: []) + optional_recommendations: list = field(default_factory=lambda: []) def to_dict(self): + test_dict = { + "name": self.name, + "description": self.description, + "expected_behavior": self.expected_behavior, + "required_result": self.required_result, + "result": self.result + } + if self.recommendations is not None and len(self.recommendations) > 0: - return { - "name": self.name, - "description": self.description, - "expected_behavior": self.expected_behavior, - "required_result": self.required_result, - "result": self.result, - "recommendations": self.recommendations - } - - return { - "name": self.name, - "description": self.description, - "expected_behavior": self.expected_behavior, - "required_result": self.required_result, - "result": self.result - } + test_dict["recommendations"] = self.recommendations + + if (self.optional_recommendations is not None + and len(self.optional_recommendations) > 0): + test_dict["optional_recommendations"] = self.optional_recommendations + + return test_dict diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py index d38f888a1..a34f70554 100644 --- a/framework/python/src/test_orc/test_orchestrator.py +++ b/framework/python/src/test_orc/test_orchestrator.py @@ -20,23 +20,34 @@ import shutil import docker from datetime import datetime -from docker.types import Mount from common import logger, util from common.testreport import TestReport -from test_orc.module import TestModule +from common.statuses import TestrunStatus, TestrunResult, TestResult +from core.docker.test_docker_module import TestModule from test_orc.test_case import TestCase +from test_orc.test_pack import TestPack import threading +from typing import List LOG_NAME = "test_orc" LOGGER = logger.get_logger("test_orc") + RUNTIME_DIR = "runtime" -RUNTIME_TEST_DIR = os.path.join(RUNTIME_DIR,"test") +RESOURCES_DIR = "resources" + +RUNTIME_TEST_DIR = os.path.join(RUNTIME_DIR, "test") +TEST_PACKS_DIR = os.path.join(RESOURCES_DIR, "test_packs") +TEST_PACK_CONFIG_FILE = "config.json" +TEST_PACK_LOGIC_FILE = "test_pack.py" + TEST_MODULES_DIR = "modules/test" MODULE_CONFIG = "conf/module_config.json" -LOG_REGEX = r"^[A-Z][a-z]{2} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} test_" + SAVED_DEVICE_REPORTS = "report/{device_folder}/" LOCAL_DEVICE_REPORTS = "local/devices/{device_folder}/reports" DEVICE_ROOT_CERTS = "local/root_certs" + +LOG_REGEX = r"^[A-Z][a-z]{2} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} test_" API_URL = "http://localhost:8000" @@ -44,22 +55,25 @@ class TestOrchestrator: """Manages and controls the test modules.""" def __init__(self, session, net_orc): - self._test_modules = [] + + self._test_modules: List[TestModule] = [] + self._test_packs: List[TestPack] = [] + self._container_logs = [] self._session = session - self._api_url = (self._session.get_api_url() + ":" + - str(self._session.get_api_port())) + + self._api_url = (self.get_session().get_api_url() + ":" + + str(self.get_session().get_api_port())) + self._net_orc = net_orc self._test_in_progress = False - self._path = os.path.dirname( - os.path.dirname( - os.path.dirname( - os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))) self._root_path = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))) + self._test_modules_running = [] + self._current_module = 0 def start(self): LOGGER.debug("Starting test orchestrator") @@ -73,6 +87,7 @@ def start(self): os.makedirs(DEVICE_ROOT_CERTS, exist_ok=True) self._load_test_modules() + self._load_test_packs() def stop(self): """Stop any running tests""" @@ -82,45 +97,112 @@ def run_test_modules(self): """Iterates through each test module and starts the container.""" # Do not start test modules if status is not in progress, e.g. Stopping - if self.get_session().get_status() != "In Progress": + if self.get_session().get_status() != TestrunStatus.IN_PROGRESS: return - device = self._session.get_target_device() + device = self.get_session().get_target_device() + test_pack_name = device.test_pack + test_pack = self.get_test_pack(test_pack_name) + LOGGER.debug("Using test pack " + test_pack.name) + self._test_in_progress = True + LOGGER.info( f"Running test modules on device with mac addr {device.mac_addr}") test_modules = [] + for module in self._test_modules: + # Ignore test modules that are just base images etc if module is None or not module.enable_container: continue + # Ignore test modules that are disabled for this device if not self._is_module_enabled(module, device): continue + num_tests = 0 + + # Add module to list of modules to run test_modules.append(module) - self.get_session().add_total_tests(len(module.tests)) - for module in test_modules: + for test in module.tests: + + # Duplicate test obj so we don't alter the source + test_copy = copy.deepcopy(test) + + # Do not add test if it is not enabled + if not self._is_test_enabled(test_copy.name, device): + continue + + # Set result to Not Started + test_copy.result = TestResult.NOT_STARTED + + # We don't want steps to resolve for not started tests + if hasattr(test_copy, "recommendations"): + test_copy.recommendations = None + + # Set the required result from the correct test pack + required_result = test_pack.get_required_result(test.name) + + test_copy.required_result = required_result + + # Add test result to the session + self.get_session().add_test_result(test_copy) + + # Increment number of tests being run by this module + num_tests += 1 + + # Increment number of tests that will be run + self.get_session().add_total_tests(num_tests) + + # Store enabled test modules in the TestOrchectrator object + self._test_modules_running = test_modules + self._current_module = 0 + + for index, module in enumerate(test_modules): + + self._current_module = index self._run_test_module(module) LOGGER.info("All tests complete") - self._session.finish() + self.get_session().finish() # Do not carry on (generating a report) if Testrun has been stopped - if self.get_session().get_status() != "In Progress": - return "Cancelled" + if self.get_session().get_status() != TestrunStatus.IN_PROGRESS: + return TestrunStatus.CANCELLED report = TestReport() - report.from_json(self._generate_report()) + + generated_report_json = self._generate_report() + report.from_json(generated_report_json) report.add_module_reports(self.get_session().get_module_reports()) + report.add_module_templates(self.get_session().get_module_templates()) device.add_report(report) self._write_reports(report) self._test_in_progress = False self.get_session().set_report_url(report.get_report_url()) + self.get_session().set_export_url(report.get_export_url()) + + # Set testing description + test_pack: TestPack = self.get_test_pack(device.test_pack) + + # Default message is empty (better than an error message). + # This should never be shown + message: str = "" + if report.get_result() == TestrunResult.COMPLIANT: + message = test_pack.get_message("compliant_description") + elif report.get_result() == TestrunResult.NON_COMPLIANT: + message = test_pack.get_message("non_compliant_description") + + self.get_session().set_description(message) + + # Set result and status at the end + self.get_session().set_result(report.get_result()) + self.get_session().set_status(report.get_status()) # Move testing output from runtime to local device folder self._timestamp_results(device) @@ -130,13 +212,11 @@ def run_test_modules(self): LOGGER.debug("Old test results cleaned") - return report.get_status() - def _write_reports(self, test_report): out_dir = os.path.join( self._root_path, RUNTIME_TEST_DIR, - self._session.get_target_device().mac_addr.replace(":", "")) + self.get_session().get_target_device().mac_addr.replace(":", "")) LOGGER.debug(f"Writing reports to {out_dir}") @@ -156,46 +236,47 @@ def _write_reports(self, test_report): def _generate_report(self): + device = self.get_session().get_target_device() + test_pack_name = device.test_pack + test_pack = self.get_test_pack(test_pack_name) + report = {} - report["testrun"] = { - "version": self.get_session().get_version() - } + report["testrun"] = {"version": self.get_session().get_version()} - report["mac_addr"] = self.get_session().get_target_device().mac_addr - report["device"] = self.get_session().get_target_device().to_dict() + report["mac_addr"] = device.mac_addr + report["device"] = device.to_dict() report["started"] = self.get_session().get_started().strftime( "%Y-%m-%d %H:%M:%S") report["finished"] = self.get_session().get_finished().strftime( "%Y-%m-%d %H:%M:%S") - report["status"] = self._calculate_result() + + # Update the result + result = test_pack.get_logic().calculate_result( + self.get_session().get_test_results()) + report["result"] = result + + # Update the status + status = test_pack.get_logic().calculate_status( + result, + self.get_session().get_test_results()) + report["status"] = status + report["tests"] = self.get_session().get_report_tests() report["report"] = ( self._api_url + "/" + SAVED_DEVICE_REPORTS.replace( "{device_folder}", - self.get_session().get_target_device().device_folder) + + device.device_folder) + self.get_session().get_started().strftime("%Y-%m-%dT%H:%M:%S")) + report["export"] = report["report"].replace("report", "export") return report - def _calculate_result(self): - result = "Compliant" - for test_result in self._session.get_test_results(): - # Check Required tests - if (test_result.required_result.lower() == "required" - and test_result.result.lower() != "compliant"): - result = "Non-Compliant" - # Check Required if Applicable tests - elif (test_result.required_result.lower() == "required if applicable" - and test_result.result.lower() == "non-compliant"): - result = "Non-Compliant" - return result - def _cleanup_old_test_results(self, device): if device.max_device_reports is not None: max_device_reports = device.max_device_reports else: - max_device_reports = self._session.get_max_device_reports() + max_device_reports = self.get_session().get_max_device_reports() if max_device_reports > 0: completed_results_dir = os.path.join( @@ -270,25 +351,92 @@ def _timestamp_results(self, device): return completed_results_dir - def zip_results(self, - device, - timestamp, - profile): + def zip_results(self, device, timestamp: str, profile): try: LOGGER.debug("Archiving test results") - src_path = os.path.join(LOCAL_DEVICE_REPORTS.replace( - "{device_folder}", - device.device_folder), - timestamp) + src_path = os.path.join( + LOCAL_DEVICE_REPORTS.replace("{device_folder}", device.device_folder), + timestamp) + + # Report file path + report_path = os.path.join( + LOCAL_DEVICE_REPORTS.replace("{device_folder}", device.device_folder), + timestamp, "test", device.mac_addr.replace(":", "")) + + # report.json path + report_json_path = os.path.join(report_path, "report.json") + + # Parse string timestamp + date_timestamp: datetime.datetime = datetime.strptime( + timestamp, "%Y-%m-%dT%H:%M:%S") + + # Find the report + test_report = None + for report in device.get_reports(): + if report.get_started() == date_timestamp: + test_report = report + + # This should not happen as the timestamp is checked in api.py first + if test_report is None: + return None + + # Load the report.json into TestReport + if os.path.exists(report_json_path): + with open(report_json_path, "r", encoding="utf-8") as report_json_file: + report_json = json.load(report_json_file) + test_report = TestReport() + test_report.from_json(report_json) + + # Copy the original report for comparison + test_report_copy = copy.deepcopy(test_report) + + # Update the report with 'additional_info' field + test_report.update_device_profile(device.additional_info) + + # Overwrite report only if additional_info has been updated + if test_report.to_json() != test_report_copy.to_json(): + + # Store the jinja templates + reload_templates = [] + + # Load the jinja templates + if os.path.isdir(report_path): + for dir_path, _, filenames in os.walk(report_path): + for filename in filenames: + try: + if filename.endswith(".j2.html"): + with open(os.path.join(dir_path, filename), "r", + encoding="utf-8") as f: + reload_templates.append(f.read()) + except Exception as e: + LOGGER.debug(f"Could not read the file: {e}") + + # Add the jinja templates to the report + test_report.add_module_templates(reload_templates) + + # Rewrite the json report + with open(os.path.join(report_path, "report.json"), + "w", + encoding="utf-8") as f: + json.dump(test_report.to_json(), f, indent=2) + + # Rewrite the html report + with open(os.path.join(report_path, "report.html"), + "w", + encoding="utf-8") as f: + f.write(test_report.to_html()) + + # Rewrite the pdf report + with open(os.path.join(report_path, "report.pdf"), "wb") as f: + f.write(test_report.to_pdf().getvalue()) # Define temp directory to store files before zipping results_dir = os.path.join(f"/tmp/testrun/{time.time()}") # Define where to save the zip file - zip_location = os.path.join("/tmp/testrun", - timestamp) + zip_location = os.path.join("/tmp/testrun", timestamp) # Delete zip_temp if it already exists if os.path.exists(results_dir): @@ -298,16 +446,13 @@ def zip_results(self, if os.path.exists(zip_location + ".zip"): os.remove(zip_location + ".zip") - shutil.copytree(src_path,results_dir) + shutil.copytree(src_path, results_dir) # Include profile if specified if profile is not None: - LOGGER.debug( - f"Copying profile {profile.name} to results directory") + LOGGER.debug(f"Copying profile {profile.name} to results directory") shutil.copy(profile.get_file_path(), - os.path.join( - results_dir, - "profile.json")) + os.path.join(results_dir, "profile.json")) with open(os.path.join(results_dir, "profile.pdf"), "wb") as f: f.write(profile.to_pdf(device).getvalue()) @@ -320,14 +465,13 @@ def zip_results(self, # Check that the ZIP was successfully created zip_file = zip_location + ".zip" - LOGGER.info(f'''Archive {'created at ' + zip_file + LOGGER.info(f"""Archive {"created at " + zip_file if os.path.exists(zip_file) - else'creation failed'}''') - + else "creation failed"}""") return zip_file - except Exception as error: # pylint: disable=W0703 + except Exception as error: # pylint: disable=W0703 LOGGER.error("Failed to create zip file") LOGGER.debug(error) return None @@ -339,6 +483,7 @@ def _is_module_enabled(self, module, device): # Enable module as fallback enabled = True + if device.test_modules is not None: test_modules = device.test_modules if module.name in test_modules: @@ -350,99 +495,50 @@ def _is_module_enabled(self, module, device): return enabled + def _is_test_enabled(self, test, device): + + test_pack_name = device.test_pack + test_pack = self.get_test_pack(test_pack_name) + + return test_pack.get_test(test) is not None + def _run_test_module(self, module): """Start the test container and extract the results.""" # Check that Testrun is not stopping - if self.get_session().get_status() != "In Progress": + if self.get_session().get_status() != TestrunStatus.IN_PROGRESS: return - device = self._session.get_target_device() + device = self.get_session().get_target_device() LOGGER.info(f"Running test module {module.name}") # Get all tests to be executed and set to in progress - for test in module.tests: + for current_test, test in enumerate(module.tests): + # Check that device is connected + if not self._net_orc.is_device_connected(): + LOGGER.error("Device was disconnected") + self._set_test_modules_error(current_test) + self.get_session().set_status(TestrunStatus.CANCELLED) + return + + # Copy the test so we don't alter the source test_copy = copy.deepcopy(test) - test_copy.result = "In Progress" + + # Update test status to in progress + test_copy.result = TestResult.IN_PROGRESS # We don't want steps to resolve for in progress tests if hasattr(test_copy, "recommendations"): test_copy.recommendations = None - self.get_session().add_test_result(test_copy) - - try: - - device_test_dir = os.path.join(self._root_path, RUNTIME_TEST_DIR, - device.mac_addr.replace(":", "")) - - container_runtime_dir = os.path.join(device_test_dir, module.name) - os.makedirs(container_runtime_dir, exist_ok=True) - - config_file = os.path.join(self._root_path, "local/system.json") - root_certs_dir = os.path.join(self._root_path, "local/root_certs") - - container_log_file = os.path.join(container_runtime_dir, "module.log") + # Only add/update the test if it is enabled + if self._is_test_enabled(test_copy.name, device): + self.get_session().add_test_result(test_copy) - network_runtime_dir = os.path.join(self._root_path, "runtime/network") - - device_startup_capture = os.path.join(device_test_dir, "startup.pcap") - util.run_command(f"chown -R {self._host_user} {device_startup_capture}") - - device_monitor_capture = os.path.join(device_test_dir, "monitor.pcap") - util.run_command(f"chown -R {self._host_user} {device_monitor_capture}") - - client = docker.from_env() - - module.container = client.containers.run( - module.image_name, - auto_remove=True, - cap_add=["NET_ADMIN"], - name=module.container_name, - hostname=module.container_name, - privileged=True, - detach=True, - mounts=[ - Mount(target="/testrun/system.json", - source=config_file, - type="bind", - read_only=True), - Mount(target="/testrun/root_certs", - source=root_certs_dir, - type="bind", - read_only=True), - Mount(target="/runtime/output", - source=container_runtime_dir, - type="bind"), - Mount(target="/runtime/network", - source=network_runtime_dir, - type="bind", - read_only=True), - Mount(target="/runtime/device/startup.pcap", - source=device_startup_capture, - type="bind", - read_only=True), - Mount(target="/runtime/device/monitor.pcap", - source=device_monitor_capture, - type="bind", - read_only=True) - ], - environment={ - "TZ": self.get_session().get_timezone(), - "HOST_USER": self._host_user, - "DEVICE_MAC": device.mac_addr, - "IPV4_ADDR": device.ip_addr, - "DEVICE_TEST_MODULES": json.dumps(device.test_modules), - "IPV4_SUBNET": self._net_orc.network_config.ipv4_network, - "IPV6_SUBNET": self._net_orc.network_config.ipv6_network - }) - except (docker.errors.APIError, - docker.errors.ContainerError) as container_error: - LOGGER.error("Test module " + module.name + " has failed to start") - LOGGER.debug(container_error) - return + # Start the test module + module.start(device) # Mount the test container to the virtual network if requried if module.network: @@ -451,7 +547,6 @@ def _run_test_module(self, module): # Determine the module timeout time test_module_timeout = time.time() + module.timeout - status = self._get_module_status(module) # Resolving container logs is blocking so we need to spawn a new thread log_stream = module.container.logs(stream=True, stdout=True, stderr=True) @@ -460,87 +555,108 @@ def _run_test_module(self, module): log_thread.daemon = True log_thread.start() - while (status == "running" and self._session.get_status() == "In Progress"): + while (module.get_status() == "running" + and self.get_session().get_status() == TestrunStatus.IN_PROGRESS): + + # Check that timeout has not exceeded if time.time() > test_module_timeout: LOGGER.error("Module timeout exceeded, killing module: " + module.name) - self._stop_module(module=module, kill=True) + module.stop(kill=True) + + # Update the test description for the tests + for test in module.tests: + + # Copy the test so we don't alter the source + test_copy = copy.deepcopy(test) + + # Update test + test_copy.result = TestResult.ERROR + test_copy.description = ( + "Module timeout exceeded. Try increasing the timeout value." + ) + self.get_session().add_test_result(test_copy) + break - status = self._get_module_status(module) # Save all container logs to file - with open(container_log_file, "w", encoding="utf-8") as f: + with open(module.container_log_file, "w", encoding="utf-8") as f: for line in self._container_logs: f.write(line + "\n") # Check that Testrun has not been stopped whilst this module was running - if self.get_session().get_status() == "Stopping": + if self.get_session().get_status() == TestrunStatus.STOPPING: # Discard results for this module LOGGER.info(f"Test module {module.name} has forcefully quit") return - # Get test results from module - container_runtime_dir = os.path.join( - self._root_path, - "runtime/test/" + device.mac_addr.replace(":", "") + "/" + module.name) - results_file = f"{container_runtime_dir}/{module.name}-result.json" + results_file = f"{module.container_runtime_dir}/{module.name}-result.json" try: with open(results_file, "r", encoding="utf-8-sig") as f: + + # Load results from JSON file module_results_json = json.load(f) module_results = module_results_json["results"] for test_result in module_results: - # Convert dict into TestCase object - test_case = TestCase( - name=test_result["name"], - description=test_result["description"], - expected_behavior=test_result["expected_behavior"], - required_result=test_result["required_result"], - result=test_result["result"]) - test_case.result=test_result["result"] - - if (test_case.result == "Non-Compliant" and - "recommendations" in test_result): + # Convert dict from json into TestCase object + test_case = TestCase(name=test_result["name"], + result=test_result["result"], + description=test_result["description"]) + + # Add steps to resolve if test is non-compliant + if (test_case.result == TestResult.NON_COMPLIANT + and "recommendations" in test_result): test_case.recommendations = test_result["recommendations"] else: - test_case.recommendations = None + test_case.recommendations = [] - self._session.add_test_result(test_case) + self.get_session().add_test_result(test_case) except (FileNotFoundError, PermissionError, json.JSONDecodeError) as results_error: LOGGER.error( - f"Error occurred whilst obtaining results for module {module.name}") + f"Error occurred whilst obtaining results for module {module.name}") LOGGER.error(results_error) # Get the markdown report from the module if generated - markdown_file = f"{container_runtime_dir}/{module.name}_report.md" + markdown_file = f"{module.container_runtime_dir}/{module.name}_report.md" try: with open(markdown_file, "r", encoding="utf-8") as f: module_report = f.read() - self._session.add_module_report(module_report) + self.get_session().add_module_report(module_report) except (FileNotFoundError, PermissionError): LOGGER.debug("Test module did not produce a markdown module report") # Get the HTML report from the module if generated - html_file = f"{container_runtime_dir}/{module.name}_report.html" + html_file = f"{module.container_runtime_dir}/{module.name}_report.html" try: with open(html_file, "r", encoding="utf-8") as f: module_report = f.read() LOGGER.debug(f"Adding module report for module {module.name}") - self._session.add_module_report(module_report) + self.get_session().add_module_report(module_report) except (FileNotFoundError, PermissionError): LOGGER.debug("Test module did not produce a html module report") + # Get the Jinja report + jinja_file = f"{module.container_runtime_dir}/{module.name}_report.j2.html" + try: + with open(jinja_file, "r", encoding="utf-8") as f: + module_template = f.read() + LOGGER.debug(f"Adding module template for module {module.name}") + self.get_session().add_module_template(module_template) + except (FileNotFoundError, PermissionError): + LOGGER.debug("Test module did not produce a module template") - LOGGER.info(f"Test module {module.name} has finished") + # LOGGER.info(f"Test module {module.name} has finished") - # Resolve all current log data in the containers log_stream - # this method is blocking so should be called in - # a thread or within a proper blocking context def _get_container_logs(self, log_stream): + """Resolve all current log data in the containers log_stream + this method is blocking so should be called in + a thread or within a proper blocking context""" self._container_logs = [] for log_chunk in log_stream: lines = log_chunk.decode("utf-8").splitlines() + # Process each line and strip blank space processed_lines = [line.strip() for line in lines if line.strip()] self._container_logs.extend(processed_lines) @@ -574,12 +690,16 @@ def _get_module_container(self, module): LOGGER.error(error) return container + def _load_test_packs(self): + + self._test_packs = TestPack.get_test_packs() + def _load_test_modules(self): """Load network modules from module_config.json.""" LOGGER.debug("Loading test modules from /" + TEST_MODULES_DIR) loaded_modules = "Loaded the following test modules: " - test_modules_dir = os.path.join(self._path, TEST_MODULES_DIR) + test_modules_dir = os.path.join(self._root_path, TEST_MODULES_DIR) module_dirs = os.listdir(test_modules_dir) # Check if the directory protocol exists and move it to the beginning @@ -587,6 +707,12 @@ def _load_test_modules(self): # corrupted during DHCP changes in the conn module if "protocol" in module_dirs: module_dirs.insert(0, module_dirs.pop(module_dirs.index("protocol"))) + # Check if the directory services exists and move it higher in the index + # so it always runs before connection. Connection may cause too many + # DHCP changes causing nmap to use wrong IP during scan + if "services" in module_dirs and "conn" in module_dirs: + module_dirs.insert(module_dirs.index("conn"), + module_dirs.pop(module_dirs.index("services"))) for module_dir in module_dirs: @@ -599,87 +725,37 @@ def _load_test_modules(self): def _load_test_module(self, module_dir): """Import module configuration from module_config.json.""" - LOGGER.debug(f"Loading test module {module_dir}") - - modules_dir = os.path.join(self._path, TEST_MODULES_DIR) - - # Load basic module information - module = TestModule() - with open(os.path.join(self._path, modules_dir, module_dir, MODULE_CONFIG), - encoding="UTF-8") as module_config_file: - module_json = json.load(module_config_file) - - module.name = module_json["config"]["meta"]["name"] - module.display_name = module_json["config"]["meta"]["display_name"] - module.description = module_json["config"]["meta"]["description"] - - if "enabled" in module_json["config"]: - module.enabled = module_json["config"]["enabled"] - - module.dir = os.path.join(self._path, modules_dir, module_dir) - module.dir_name = module_dir - module.build_file = module_dir + ".Dockerfile" - module.container_name = "tr-ct-" + module.dir_name + "-test" - module.image_name = "test-run/" + module.dir_name + "-test" - - # Load test cases - if "tests" in module_json["config"]: - module.total_tests = len(module_json["config"]["tests"]) - for test_case_json in module_json["config"]["tests"]: - try: - test_case = TestCase( - name=test_case_json["name"], - description=test_case_json["test_description"], - expected_behavior=test_case_json["expected_behavior"], - required_result=test_case_json["required_result"] - ) + # Resolve the main docker interface (docker0) for host interaction + # Can't use device or internet iface since these are not in a stable + # state for this type of communication during testing but docker0 has + # to exist and should always be available + external_ip = self._net_orc.get_ip_address("docker0") + extra_hosts = { + "external.localhost": external_ip + } if external_ip is not None else {} - if "recommendations" in test_case_json: - test_case.recommendations = test_case_json["recommendations"] - module.tests.append(test_case) - except Exception as error: # pylint: disable=W0718 - LOGGER.error("Failed to load test case. See error for details") - LOGGER.error(error) - - if "timeout" in module_json["config"]["docker"]: - module.timeout = module_json["config"]["docker"]["timeout"] - - # Determine if this is a container or just an image/template - if "enable_container" in module_json["config"]["docker"]: - module.enable_container = module_json["config"]["docker"][ - "enable_container"] - - # Determine if this module needs network access - if "network" in module_json["config"]: - module.network = module_json["config"]["network"] - - # Ensure container is built after any dependencies - if "depends_on" in module_json["config"]["docker"]: - depends_on_module = module_json["config"]["docker"]["depends_on"] - if self._get_test_module(depends_on_module) is None: - self._load_test_module(depends_on_module) - - self._test_modules.append(module) - return module - - def build_test_modules(self): - """Build all test modules.""" - LOGGER.info("Building test modules...") - for module in self._test_modules: - self._build_test_module(module) + # Make sure we only load each module once since some modules will + # depend on the same module + if not any(m.dir_name == module_dir for m in self._test_modules): - def _build_test_module(self, module): - LOGGER.debug("Building docker image for module " + module.dir_name) + modules_dir = os.path.join(self._root_path, TEST_MODULES_DIR) - client = docker.from_env() - try: - client.images.build( - dockerfile=os.path.join(module.dir, module.build_file), - path=self._path, - forcerm=True, # Cleans up intermediate containers during build - tag=module.image_name) - except docker.errors.BuildError as error: - LOGGER.error(error) + module_conf_file = os.path.join(self._root_path, modules_dir, module_dir, + MODULE_CONFIG) + + module = TestModule(module_conf_file, self, self.get_session(), + extra_hosts) + if module.depends_on is not None: + self._load_test_module(module.depends_on) + self._test_modules.append(module) + + return module + + def get_test_packs(self) -> List[TestPack]: + return self._test_packs + + def get_test_pack(self, name: str) -> TestPack: + return TestPack.get_test_pack(name, self._test_packs) def _stop_modules(self, kill=False): LOGGER.info("Stopping test modules") @@ -692,18 +768,7 @@ def _stop_modules(self, kill=False): def _stop_module(self, module, kill=False): LOGGER.debug("Stopping test module " + module.container_name) - try: - container = module.container - if container is not None: - if kill: - LOGGER.debug("Killing container:" + module.container_name) - container.kill() - else: - LOGGER.debug("Stopping container:" + module.container_name) - container.stop() - LOGGER.debug("Container stopped:" + module.container_name) - except docker.errors.NotFound: - pass + module.stop(kill=kill) def get_test_modules(self): return self._test_modules @@ -729,3 +794,12 @@ def get_test_case(self, name): def get_session(self): return self._session + + def _set_test_modules_error(self, current_test): + """Set all remaining tests to error""" + for i in range(self._current_module, len(self._test_modules_running)): + start_idx = current_test if i == self._current_module else 0 + for j in range(start_idx, len(self._test_modules_running[i].tests)): + self.get_session().set_test_result_error( + self._test_modules_running[i].tests[j], + "Test did not run, the device was disconnected") diff --git a/framework/python/src/test_orc/test_pack.py b/framework/python/src/test_orc/test_pack.py new file mode 100644 index 000000000..eb9c852c2 --- /dev/null +++ b/framework/python/src/test_orc/test_pack.py @@ -0,0 +1,133 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Represents a testing pack.""" +from types import ModuleType +from typing import List, Dict +from dataclasses import dataclass, field +from collections import defaultdict +import os +import sys +import json +import importlib + +RESOURCES_DIR = "resources" + +TEST_PACKS_DIR = os.path.join(RESOURCES_DIR, "test_packs") +TEST_PACK_CONFIG_FILE = "config.json" +TEST_PACK_LOGIC_FILE = "test_pack.py" + + +@dataclass +class TestPack: # pylint: disable=too-few-public-methods,too-many-instance-attributes + """Represents a test pack.""" + + name: str = "undefined" + description: str = "" + tests: List[dict] = field(default_factory=lambda: []) + language: Dict = field(default_factory=lambda: defaultdict(dict)) + pack_logic: ModuleType = None + path: str = "" + + def get_test(self, test_name: str) -> str: + """Get details of a test from the test pack""" + + for test in self.tests: + if "name" in test and test["name"].lower() == test_name.lower(): + return test + + def get_required_result(self, test_name: str) -> str: + """Fetch the required result of the test""" + + test = self.get_test(test_name) + + if test is not None and "required_result" in test: + return test["required_result"] + + return "Informational" + + def get_logic(self): + return self.pack_logic + + def get_message(self, name: str) -> str: + if name in self.language: + return self.language[name] + return "Message not found" + + def to_dict(self): + return { + "name": self.name, + "description": self.description, + "tests": self.tests, + "language": self.language + } + + @staticmethod + def load_logic(source, module_name=None): + """Reads file source and loads it as a module""" + + spec = importlib.util.spec_from_file_location(module_name, source) + module = importlib.util.module_from_spec(spec) + + # Add the module to sys.modules + sys.modules[module_name] = module + + # Execute the module + spec.loader.exec_module(module) + + return module + + @staticmethod + def get_test_packs() -> List["TestPack"]: + + root_path = os.path.dirname( + os.path.dirname( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))) + test_packs = [] + + for test_pack_folder in os.listdir(TEST_PACKS_DIR): + test_pack_path = os.path.join( + root_path, + TEST_PACKS_DIR, + test_pack_folder + ) + + with open(os.path.join( + test_pack_path, + TEST_PACK_CONFIG_FILE), encoding="utf-8") as f: + test_pack_json = json.load(f) + + test_pack: TestPack = TestPack( + name = test_pack_json["name"], + tests = test_pack_json["tests"], + language = test_pack_json["language"], + pack_logic = TestPack.load_logic( + os.path.join(test_pack_path, TEST_PACK_LOGIC_FILE), + "test_pack_" + test_pack_folder + "_logic" + ), + path = test_pack_path + ) + test_packs.append(test_pack) + + return test_packs + + @staticmethod + def get_test_pack(name: str, test_packs: List["TestPack"]=None) -> "TestPack": + if test_packs is None: + test_packs = TestPack.get_test_packs() + for test_pack in test_packs: + if test_pack.name.lower() == name.lower(): + return test_pack + return None diff --git a/framework/requirements.txt b/framework/requirements.txt index c31978d99..543b8ff5e 100644 --- a/framework/requirements.txt +++ b/framework/requirements.txt @@ -1,8 +1,8 @@ # Requirements for the core module -requests<2.32.0 +requests==2.32.3 # Requirements for the net_orc module -docker==7.0.0 +docker==7.1.0 ipaddress==1.0.23 netifaces==0.11.0 scapy==2.5.0 @@ -15,19 +15,31 @@ pydyf==0.8.0 fastapi==0.109.1 psutil==5.9.8 uvicorn==0.27.0 -python-multipart==0.0.9 +python-multipart==0.0.19 pydantic==2.7.1 # Requirements for testing pytest==7.4.4 pytest-timeout==2.2.0 +responses==0.25.3 + # Requirements for the report markdown==3.5.2 # Requirements for the session -cryptography==42.0.7 +cryptography==44.0.1 pytz==2024.1 # Requirements for the risk profile python-dateutil==2.9.0 + +# Requirements for MQTT client +paho-mqtt==2.1.0 + +# Requirements for background tasks +APScheduler==3.10.4 + +# Requirements for reports generation +Jinja2==3.1.6 +beautifulsoup4==4.12.3 diff --git a/local/system.json.example b/local/system.json.example index 23023bead..43527240f 100644 --- a/local/system.json.example +++ b/local/system.json.example @@ -1,10 +1,12 @@ { "network": { - "device_intf": "enx123456789123", - "internet_intf": "enx123456789124" + "device_intf": "", + "internet_intf": "" }, "log_level": "INFO", "startup_timeout": 60, "monitor_period": 300, - "max_device_reports": 0 + "allow_disconnect": false, + "max_device_reports": 0, + "org_name": "" } diff --git a/make/DEBIAN/control b/make/DEBIAN/control index 488f69458..c45dd27c9 100644 --- a/make/DEBIAN/control +++ b/make/DEBIAN/control @@ -1,5 +1,5 @@ Package: Testrun -Version: 1.3.1 +Version: 2.2 Architecture: amd64 Maintainer: Google Homepage: https://github.com/google/testrun diff --git a/modules/devices/faux-dev/bin/start_network_service b/modules/devices/faux-dev/bin/start_network_service index d4bb8a92d..7d689f9dd 100644 --- a/modules/devices/faux-dev/bin/start_network_service +++ b/modules/devices/faux-dev/bin/start_network_service @@ -35,7 +35,7 @@ else INTF=$DEFINED_IFACE fi -#Create and set permissions on the output files +# Create and set permissions on the output files OUTPUT_DIR=/runtime/validation/ LOG_FILE=$OUTPUT_DIR/$MODULE_NAME.log RESULT_FILE=$OUTPUT_DIR/result.json diff --git a/modules/devices/faux-dev/faux-dev.Dockerfile b/modules/devices/faux-dev/faux-dev.Dockerfile index ecfdfc5c2..18901a2a1 100644 --- a/modules/devices/faux-dev/faux-dev.Dockerfile +++ b/modules/devices/faux-dev/faux-dev.Dockerfile @@ -12,13 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/faux-dev -FROM test-run/base:latest +# Image name: testrun/faux-dev +FROM testrun/base:latest ARG MODULE_NAME=faux-dev ARG MODULE_DIR=modules/devices/$MODULE_NAME +ARG COMMON_DIR=framework/python/src/common -#Update and get all additional requirements not contained in the base image +# Update and get all additional requirements not contained in the base image RUN apt-get update --fix-missing # NTP requireds interactive installation so we're going to turn that off @@ -34,4 +35,4 @@ COPY $MODULE_DIR/conf /testrun/conf COPY $MODULE_DIR/bin /testrun/bin # Copy over all python files -COPY $MODULE_DIR/python /testrun/python \ No newline at end of file +COPY $MODULE_DIR/python /testrun/python diff --git a/modules/devices/faux-dev/python/src/logger.py b/modules/devices/faux-dev/python/src/logger.py deleted file mode 100644 index a727ad7bb..000000000 --- a/modules/devices/faux-dev/python/src/logger.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Sets up the logger to be used for the faux-device.""" - -import json -import logging -import os - -LOGGERS = {} -_LOG_FORMAT = '%(asctime)s %(name)-8s %(levelname)-7s %(message)s' -_DATE_FORMAT = '%b %02d %H:%M:%S' -_CONF_DIR = 'conf' -_CONF_FILE_NAME = 'system.json' -_LOG_DIR = '/runtime/validation' - -# Set log level -with open(os.path.join(_CONF_DIR, _CONF_FILE_NAME), - encoding='utf-8') as conf_file: - system_conf_json = json.load(conf_file) - -log_level_str = system_conf_json['log_level'] -log_level = logging.getLevelName(log_level_str) - -log_format = logging.Formatter(fmt=_LOG_FORMAT, datefmt=_DATE_FORMAT) - - -def add_file_handler(log, log_file): - """Add file handler to existing log.""" - handler = logging.FileHandler(os.path.join(_LOG_DIR, log_file + '.log')) - handler.setFormatter(log_format) - log.addHandler(handler) - - -def add_stream_handler(log): - """Add stream handler to existing log.""" - handler = logging.StreamHandler() - handler.setFormatter(log_format) - log.addHandler(handler) - - -def get_logger(name, log_file=None): - """Return logger for requesting class.""" - if name not in LOGGERS: - LOGGERS[name] = logging.getLogger(name) - LOGGERS[name].setLevel(log_level) - add_stream_handler(LOGGERS[name]) - if log_file is not None: - add_file_handler(LOGGERS[name], log_file) - return LOGGERS[name] diff --git a/modules/devices/faux-dev/python/src/util.py b/modules/devices/faux-dev/python/src/util.py deleted file mode 100644 index 81f9d2ced..000000000 --- a/modules/devices/faux-dev/python/src/util.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Provides basic utilities for the faux-device.""" -import subprocess -import shlex - - -def run_command(cmd, logger, output=True): - """Runs a process at the os level - By default, returns the standard output and error output - If the caller sets optional output parameter to False, - will only return a boolean result indicating if it was - successful in running the command. Failure is indicated - by any return code from the process other than zero.""" - - success = False - with subprocess.Popen( - shlex.split(cmd), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as process: - - stdout, stderr = process.communicate() - - if process.returncode != 0: - err_msg = f'{stderr.strip()}. Code: {process.returncode}' - logger.error('Command Failed: ' + cmd) - logger.error('Error: ' + err_msg) - else: - success = True - logger.debug('Command succeeded: ' + cmd) - if output: - out = stdout.strip().decode('utf-8') - logger.debug('Command output: ' + out) - return success, out - else: - return success, None diff --git a/modules/network/base/base.Dockerfile b/modules/network/base/base.Dockerfile index b30f6a7d9..7f6edb409 100644 --- a/modules/network/base/base.Dockerfile +++ b/modules/network/base/base.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/base -FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 +# Image name: testrun/base +FROM ubuntu@sha256:77d57fd89366f7d16615794a5b53e124d742404e20f035c22032233f1826bd6a RUN apt-get update @@ -30,8 +30,9 @@ COPY $COMMON_DIR/ /testrun/python/src/common # Setup the base python requirements COPY $MODULE_DIR/python /testrun/python -# Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +# Install all python requirements for the module +# --break-system-packages flag used to bypass PEP668 +RUN pip3 install --break-system-packages -r /testrun/python/requirements.txt # Add the bin files COPY $MODULE_DIR/bin /testrun/bin @@ -42,5 +43,5 @@ RUN dos2unix /testrun/bin/* # Make sure all the bin files are executable RUN chmod u+x /testrun/bin/* -#Start the network module +# Start the network module ENTRYPOINT [ "/testrun/bin/start_module" ] \ No newline at end of file diff --git a/modules/network/base/bin/start_module b/modules/network/base/bin/start_module index 8e8cb5e4b..fb2823afd 100644 --- a/modules/network/base/bin/start_module +++ b/modules/network/base/bin/start_module @@ -22,7 +22,7 @@ DEFAULT_IFACE=veth0 # Create a local user that matches the same as the host # to be used for correct file ownership for various logs -# HOST_USER mapped in via docker container environemnt variables +# HOST_USER mapped in via docker container environment variables useradd $HOST_USER # Enable IPv6 for all containers @@ -42,6 +42,7 @@ fi # Extract the necessary config parameters MODULE_NAME=$(echo "$CONF" | jq -r '.config.meta.name') DEFINED_IFACE=$(echo "$CONF" | jq -r '.config.network.interface') +HOST=$(echo "$CONF" | jq -r '.config.network.host') GRPC=$(echo "$CONF" | jq -r '.config.grpc') # Validate the module name is present @@ -70,14 +71,19 @@ $BIN_DIR/setup_binaries $BIN_DIR echo "Starting module $MODULE_NAME on local interface $INTF..." -# Wait for interface to become ready -$BIN_DIR/wait_for_interface $INTF +# Only non-host containers will have a specific +# interface for capturing +if [[ "$HOST" != "true" ]]; then -# Small pause to let the interface stabalize before starting the capture -#sleep 1 + # Wait for interface to become ready + $BIN_DIR/wait_for_interface $INTF -# Start network capture -$BIN_DIR/capture $MODULE_NAME $INTF + # Small pause to let the interface stabalize before starting the capture + #sleep 1 + + # Start network capture + $BIN_DIR/capture $MODULE_NAME $INTF +fi # Start the grpc server if [[ ! -z $GRPC && ! $GRPC == "null" ]] @@ -96,4 +102,4 @@ fi sleep 3 # Start the networking service -$BIN_DIR/start_network_service $MODULE_NAME $INTF \ No newline at end of file +$BIN_DIR/start_network_service $MODULE_NAME $INTF diff --git a/modules/network/base/python/requirements.txt b/modules/network/base/python/requirements.txt index 9d9473d74..01abf05ce 100644 --- a/modules/network/base/python/requirements.txt +++ b/modules/network/base/python/requirements.txt @@ -1,3 +1,10 @@ -grpcio -grpcio-tools -netifaces \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +protobuf==5.28.3 + +# User defined packages +grpcio==1.67.1 +grpcio-tools==1.67.1 +netifaces==0.11.0 + diff --git a/modules/network/base/python/src/grpc_server/start_server.py b/modules/network/base/python/src/grpc_server/start_server.py index d372949e5..9c34ec736 100644 --- a/modules/network/base/python/src/grpc_server/start_server.py +++ b/modules/network/base/python/src/grpc_server/start_server.py @@ -46,6 +46,5 @@ def run(): print('gRPC server starting on port ' + port) serve(port) - if __name__ == '__main__': run() diff --git a/modules/network/dhcp-1/dhcp-1.Dockerfile b/modules/network/dhcp-1/dhcp-1.Dockerfile index e50ed9a95..7df94b4fd 100644 --- a/modules/network/dhcp-1/dhcp-1.Dockerfile +++ b/modules/network/dhcp-1/dhcp-1.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/dhcp-primary -FROM test-run/base:latest +# Image name: testrun/dhcp-primary +FROM testrun/base:latest ARG MODULE_NAME=dhcp-1 ARG MODULE_DIR=modules/network/$MODULE_NAME diff --git a/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py b/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py index aa2945759..e2318ac02 100644 --- a/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py @@ -68,8 +68,8 @@ def get_leases(self): leases.append(lease) except Exception as e: # pylint: disable=W0718 # Let non lease lines file without extra checks - LOGGER.error('Making Lease Error: ' + str(e)) - LOGGER.error('Not a valid lease line: ' + line) + LOGGER.info('Not a valid lease line: ' + line) + LOGGER.error('Get lease error: ' + str(e)) return leases def delete_lease(self, ip_addr): diff --git a/modules/network/dhcp-2/dhcp-2.Dockerfile b/modules/network/dhcp-2/dhcp-2.Dockerfile index 66ea857c3..4dcd7a819 100644 --- a/modules/network/dhcp-2/dhcp-2.Dockerfile +++ b/modules/network/dhcp-2/dhcp-2.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/dhcp-primary -FROM test-run/base:latest +# Image name: testrun/dhcp-primary +FROM testrun/base:latest ARG MODULE_NAME=dhcp-2 ARG MODULE_DIR=modules/network/$MODULE_NAME diff --git a/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py b/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py index 08e6feabe..f6db83094 100644 --- a/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py +++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py @@ -58,9 +58,9 @@ def get_leases(self): leases = [] lease_list_raw = self._get_lease_list() LOGGER.info('Raw Leases:\n' + str(lease_list_raw) + '\n') - lease_list_start = lease_list_raw.find('=========',0) - lease_list_start = lease_list_raw.find('\n',lease_list_start) - lease_list = lease_list_raw[lease_list_start+1:] + lease_list_start = lease_list_raw.find('=========', 0) + lease_list_start = lease_list_raw.find('\n', lease_list_start) + lease_list = lease_list_raw[lease_list_start + 1:] lines = lease_list.split('\n') for line in lines: try: @@ -68,8 +68,8 @@ def get_leases(self): leases.append(lease) except Exception as e: # pylint: disable=W0718 # Let non lease lines file without extra checks - LOGGER.error('Making Lease Error: ' + str(e)) - LOGGER.error('Not a valid lease line: ' + line) + LOGGER.info('Not a valid lease line: ' + line) + LOGGER.error('Get lease error: ' + str(e)) return leases def delete_lease(self, ip_addr): diff --git a/modules/network/dns/dns.Dockerfile b/modules/network/dns/dns.Dockerfile index d59b8a391..2b46dfb4a 100644 --- a/modules/network/dns/dns.Dockerfile +++ b/modules/network/dns/dns.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/dns -FROM test-run/base:latest +# Image name: testrun/dns +FROM testrun/base:latest ARG MODULE_NAME=dns ARG MODULE_DIR=modules/network/$MODULE_NAME diff --git a/modules/network/gateway/gateway.Dockerfile b/modules/network/gateway/gateway.Dockerfile index 885e4a9f0..2b72174ab 100644 --- a/modules/network/gateway/gateway.Dockerfile +++ b/modules/network/gateway/gateway.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/gateway -FROM test-run/base:latest +# Image name: testrun/gateway +FROM testrun/base:latest ARG MODULE_NAME=gateway ARG MODULE_DIR=modules/network/$MODULE_NAME diff --git a/testing/unit/build.sh b/modules/network/host/bin/start_network_service similarity index 82% rename from testing/unit/build.sh rename to modules/network/host/bin/start_network_service index db84e0299..b94b6ff7c 100644 --- a/testing/unit/build.sh +++ b/modules/network/host/bin/start_network_service @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash # Copyright 2023 Google LLC # @@ -14,4 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -sudo docker build -f testing/unit/unit_test.Dockerfile -t testrun/unit-test . \ No newline at end of file +echo "Starting host service..." + +# Keep host container running until stopped +while true; do + sleep 3 +done diff --git a/modules/network/host/conf/module_config.json b/modules/network/host/conf/module_config.json new file mode 100644 index 000000000..87ec39a35 --- /dev/null +++ b/modules/network/host/conf/module_config.json @@ -0,0 +1,24 @@ +{ + "config": { + "meta": { + "name": "host", + "display_name": "Host", + "description": "Used to access host level networking operations" + }, + "network": { + "host": true + }, + "grpc":{ + "port": 5001 + }, + "docker": { + "depends_on": "base", + "mounts": [ + { + "source": "runtime/network", + "target": "/runtime/network" + } + ] + } + } +} \ No newline at end of file diff --git a/modules/network/host/host.Dockerfile b/modules/network/host/host.Dockerfile new file mode 100644 index 000000000..60c8bf59a --- /dev/null +++ b/modules/network/host/host.Dockerfile @@ -0,0 +1,34 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Image name: testrun/host +FROM testrun/base:latest + +ARG MODULE_NAME=host +ARG MODULE_DIR=modules/network/$MODULE_NAME + +#Update and get all additional requirements not contained in the base image +RUN apt-get update --fix-missing + +# Install all necessary packages +RUN apt-get install -y net-tools ethtool + +# Copy over all configuration files +COPY $MODULE_DIR/conf /testrun/conf + +# Copy over all binary files +COPY $MODULE_DIR/bin /testrun/bin + +# Copy over all python files +COPY $MODULE_DIR/python /testrun/python diff --git a/modules/network/host/python/src/grpc_server/network_service.py b/modules/network/host/python/src/grpc_server/network_service.py new file mode 100644 index 000000000..cbb3a1b7a --- /dev/null +++ b/modules/network/host/python/src/grpc_server/network_service.py @@ -0,0 +1,120 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""gRPC Network Service for the Host network module""" +import proto.grpc_pb2_grpc as pb2_grpc +import proto.grpc_pb2 as pb2 + +import traceback +from common import logger +from common import util + +LOG_NAME = 'network_service' +LOGGER = None + + +class NetworkService(pb2_grpc.HostNetworkModule): + """gRPC endpoints for the Host container""" + + def __init__(self): + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'host') + + def CheckInterfaceStatus(self, request, context): # pylint: disable=W0613 + try: + status = self.check_interface_status(request.iface_name) + return pb2.CheckInterfaceStatusResponse(code=200, status=status) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to read iface status: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.CheckInterfaceStatusResponse(code=500, status=False) + + def GetIfaceConnectionStats(self, request, context): # pylint: disable=W0613 + try: + stats = self.get_iface_connection_stats(request.iface_name) + return pb2.GetIfaceStatsResponse(code=200, stats=stats) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to read connection stats: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.GetIfaceStatsResponse(code=500, stats=False) + + def GetIfacePortStats(self, request, context): # pylint: disable=W0613 + try: + stats = self.get_iface_port_stats(request.iface_name) + return pb2.GetIfaceStatsResponse(code=200, stats=stats) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to read port stats: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.GetIfaceStatsResponse(code=500, stats=False) + + def SetIfaceDown(self, request, context): # pylint: disable=W0613 + try: + success = self.set_interface_down(request.iface_name) + return pb2.SetIfaceResponse(code=200, success=success) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to set interface down: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.SetIfaceResponse(code=500, success=False) + + def SetIfaceUp(self, request, context): # pylint: disable=W0613 + try: + success = self.set_interface_up(request.iface_name) + return pb2.SetIfaceResponse(code=200, success=success) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to set interface up: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.SetIfaceResponse(code=500, success=False) + + def check_interface_status(self, interface_name): + output = util.run_command(cmd=f'ip link show {interface_name}', output=True) + if 'state DOWN ' in output[0]: + return False + else: + return True + + def get_iface_connection_stats(self, iface): + """Extract information about the physical connection""" + response = util.run_command(f'ethtool {iface}') + if len(response[1]) == 0: + return response[0] + else: + return None + + def get_iface_port_stats(self, iface): + """Extract information about packets connection""" + response = util.run_command(f'ethtool -S {iface}') + if len(response[1]) == 0: + return response[0] + else: + return None + + def set_interface_up(self, interface_name): + """Set the interface to the up state""" + response = util.run_command('ip link set dev ' + interface_name + ' up') + if len(response[1]) == 0: + return response[0] + else: + return None + + def set_interface_down(self, interface_name): + """Set the interface to the up state""" + response = util.run_command('ip link set dev ' + interface_name + ' down') + if len(response[1]) == 0: + return response[0] + else: + return None diff --git a/modules/network/host/python/src/grpc_server/proto/grpc.proto b/modules/network/host/python/src/grpc_server/proto/grpc.proto new file mode 100644 index 000000000..c881b13f7 --- /dev/null +++ b/modules/network/host/python/src/grpc_server/proto/grpc.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +service HostNetworkModule { + + rpc CheckInterfaceStatus(CheckInterfaceStatusRequest) returns (CheckInterfaceStatusResponse) {}; + rpc GetIfaceConnectionStats(GetIfaceStatsRequest) returns (GetIfaceStatsResponse) {}; + rpc SetIfaceDown(SetIfaceRequest) returns (SetIfaceResponse) {}; + rpc SetIfaceUp(SetIfaceRequest) returns (SetIfaceResponse) {}; +} + +message CheckInterfaceStatusRequest { + string iface_name = 1; +} + +message CheckInterfaceStatusResponse { + int32 code = 1; + bool status = 2; +} + +message GetIfaceStatsRequest { + string iface_name = 1; +} + +message GetIfaceStatsResponse { + int32 code = 1; + string stats = 2; +} + +message SetIfaceRequest { + string iface_name = 1; +} + +message SetIfaceResponse { + int32 code = 1; + bool success = 2; +} + diff --git a/modules/network/host/python/src/grpc_server/start_server.py b/modules/network/host/python/src/grpc_server/start_server.py new file mode 100644 index 000000000..962277188 --- /dev/null +++ b/modules/network/host/python/src/grpc_server/start_server.py @@ -0,0 +1,50 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base class for starting the gRPC server for a network module.""" +from concurrent import futures +import grpc +import proto.grpc_pb2_grpc as pb2_grpc +from network_service import NetworkService +import argparse + +DEFAULT_PORT = '5001' + + +def serve(port): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + pb2_grpc.add_HostNetworkModuleServicer_to_server(NetworkService(), server) + server.add_insecure_port('[::]:' + port) + server.start() + server.wait_for_termination() + + +def run(): + parser = argparse.ArgumentParser( + description='GRPC Server for Network Module', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('-p', + '--port', + default=DEFAULT_PORT, + help='Define the default port to run the server on.') + + args = parser.parse_args() + + port = args.port + + print('gRPC server starting on port ' + port) + serve(port) + +if __name__ == '__main__': + run() diff --git a/modules/network/ntp/ntp.Dockerfile b/modules/network/ntp/ntp.Dockerfile index aa6f63e3f..d047770ef 100644 --- a/modules/network/ntp/ntp.Dockerfile +++ b/modules/network/ntp/ntp.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/ntp -FROM test-run/base:latest +# Image name: testrun/ntp +FROM testrun/base:latest ARG MODULE_NAME=ntp ARG MODULE_DIR=modules/network/$MODULE_NAME @@ -21,6 +21,8 @@ ARG MODULE_DIR=modules/network/$MODULE_NAME # Set DEBIAN_FRONTEND to noninteractive mode ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update + # Install all necessary packages RUN apt-get install -y chrony diff --git a/modules/network/ntp/python/src/ntp_server.py b/modules/network/ntp/python/src/ntp_server.py index 42fe21e77..fbe3ac17e 100644 --- a/modules/network/ntp/python/src/ntp_server.py +++ b/modules/network/ntp/python/src/ntp_server.py @@ -38,7 +38,7 @@ def is_running(self): if __name__ == '__main__': ntp = NTPServer() ntp.start() - # give some time for the server to start + # Give some time for the server to start running = False for _ in range(10): running = ntp.is_running() diff --git a/modules/network/radius/bin/start_network_service b/modules/network/radius/bin/start_network_service index d285c20d9..aad840c3a 100644 --- a/modules/network/radius/bin/start_network_service +++ b/modules/network/radius/bin/start_network_service @@ -27,7 +27,7 @@ cp $CONF_DIR/ca.crt /etc/ssl/certs/ca-certificates.crt python3 -u $PYTHON_SRC_DIR/authenticator.py & -#Create and set permissions on the log file +# Create and set permissions on the log file touch $LOG_FILE chown $HOST_USER $LOG_FILE diff --git a/modules/network/radius/conf/module_config.json b/modules/network/radius/conf/module_config.json index ce8fbd52f..0a35cc194 100644 --- a/modules/network/radius/conf/module_config.json +++ b/modules/network/radius/conf/module_config.json @@ -1,5 +1,6 @@ { "config": { + "enabled": false, "meta": { "name": "radius", "display_name": "Radius", diff --git a/modules/network/radius/python/requirements.txt b/modules/network/radius/python/requirements.txt index 37d126cb1..c814da515 100644 --- a/modules/network/radius/python/requirements.txt +++ b/modules/network/radius/python/requirements.txt @@ -1,3 +1,11 @@ -eventlet -pbr -transitions \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +dnspython==2.6.1 +greenlet==3.0.3 +six==1.16.0 + +# User defined packages +eventlet==0.36.1 +pbr==6.1.0 +transitions==0.9.2 diff --git a/modules/network/radius/radius.Dockerfile b/modules/network/radius/radius.Dockerfile index 4c8f8fac5..802480ce9 100644 --- a/modules/network/radius/radius.Dockerfile +++ b/modules/network/radius/radius.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/radius -FROM test-run/base:latest +# Image name: testrun/radius +FROM testrun/base:latest ARG MODULE_NAME=radius ARG MODULE_DIR=modules/network/$MODULE_NAME @@ -25,7 +25,8 @@ RUN apt-get update && apt-get install -y openssl freeradius git RUN git clone --branch 0.0.25 https://github.com/faucetsdn/chewie # Install chewie as Python module -RUN pip3 install chewie/ +# --break-system-packages flag used to bypass PEP668 +RUN pip3 install --break-system-packages chewie/ EXPOSE 1812/udp EXPOSE 1813/udp @@ -40,4 +41,5 @@ COPY $MODULE_DIR/bin /testrun/bin COPY $MODULE_DIR/python /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt \ No newline at end of file +# --break-system-packages flag used to bypass PEP668 +RUN pip3 install --break-system-packages -r /testrun/python/requirements.txt \ No newline at end of file diff --git a/modules/network/template/template.Dockerfile b/modules/network/template/template.Dockerfile index 1c3060496..265725258 100644 --- a/modules/network/template/template.Dockerfile +++ b/modules/network/template/template.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/template -FROM test-run/base:latest +# Image name: testrun/template +FROM testrun/base:latest ARG MODULE_NAME=template ARG MODULE_DIR=modules/network/$MODULE_NAME diff --git a/modules/test/base/README.md b/modules/test/base/README.md index e7f05d80e..24a725607 100644 --- a/modules/test/base/README.md +++ b/modules/test/base/README.md @@ -14,6 +14,13 @@ The ```config/module_config.json``` provides the name and description of the mod Within the ```python/src``` directory, basic logging and environment variables are provided to the test module. +Within the ```usr/local/etc``` directory there is a local copy of the MAC OUI database. This is just in case a new copy is unable to be downloaded during the install or update process. + +## GRPC server +Within the python directory, GRPC client code is provided to allow test modules to programmatically modify the various network services provided by Testrun. + +These currently include obtaining information about and controlling the DHCP servers in failover configuration. + ## Tests covered No tests are run by this module \ No newline at end of file diff --git a/modules/test/base/base.Dockerfile b/modules/test/base/base.Dockerfile index 4d8c0399a..7a82301a7 100644 --- a/modules/test/base/base.Dockerfile +++ b/modules/test/base/base.Dockerfile @@ -12,17 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/base-test -FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 +# Builder stage +# Image name: testrun/base-test +FROM python:3.10-slim AS builder ARG MODULE_NAME=base ARG MODULE_DIR=modules/test/$MODULE_NAME ARG COMMON_DIR=framework/python/src/common -RUN apt-get update +# Install additional requirements needed to build python packages +RUN apt-get update && \ + apt-get install -y gcc dos2unix -# Install common software -RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq net-tools iputils-ping tzdata tcpdump iproute2 jq python3 python3-pip dos2unix nmap wget --fix-missing +# Create the virtual environment +RUN python -m venv /opt/venv + +# Activate the virtual environment +ENV PATH="/opt/venv/bin:$PATH" # Install common python modules COPY $COMMON_DIR/ /testrun/python/src/common @@ -31,7 +37,7 @@ COPY $COMMON_DIR/ /testrun/python/src/common COPY $MODULE_DIR/python /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin @@ -49,6 +55,7 @@ ARG CONTAINER_PROTO_DIR=testrun/python/src/grpc_server/proto COPY $NET_MODULE_DIR/dhcp-1/$NET_MODULE_PROTO_DIR $CONTAINER_PROTO_DIR/dhcp1/ COPY $NET_MODULE_DIR/dhcp-2/$NET_MODULE_PROTO_DIR $CONTAINER_PROTO_DIR/dhcp2/ +COPY $NET_MODULE_DIR/host/$NET_MODULE_PROTO_DIR $CONTAINER_PROTO_DIR/host/ # Copy the cached version of oui.txt incase the download fails RUN mkdir -p /usr/local/etc @@ -57,5 +64,29 @@ COPY $MODULE_DIR/usr/local/etc/oui.txt /usr/local/etc/oui.txt # Update the oui.txt file from ieee RUN wget https://standards-oui.ieee.org/oui.txt -O /usr/local/etc/oui.txt || echo "Unable to update the MAC OUI database" +# Operational stage +FROM python:3.10-slim + +# Install common software +RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -yq net-tools iputils-ping tzdata tcpdump iproute2 jq dos2unix nmap wget procps --fix-missing + +# Get the virtual environment from builder stage +COPY --from=builder /opt/venv /opt/venv + +# Copy over all testrun files from the builder stage +COPY --from=builder /testrun /testrun +COPY --from=builder /usr/local/etc/oui.txt /usr/local/etc/oui.txt + +# Activate the virtual environment by setting the PATH +ENV PATH="/opt/venv/bin:$PATH" + +# Common resource folder +ENV REPORT_TEMPLATE_PATH=/testrun/resources +# Jinja base template +ENV BASE_TEMPLATE_FILE=module_report_base.jinja2 + +# Copy base template +COPY resources/report/$BASE_TEMPLATE_FILE $REPORT_TEMPLATE_PATH/ + # Start the test module ENTRYPOINT [ "/testrun/bin/start" ] \ No newline at end of file diff --git a/modules/test/base/bin/setup b/modules/test/base/bin/setup new file mode 100644 index 000000000..514466548 --- /dev/null +++ b/modules/test/base/bin/setup @@ -0,0 +1,81 @@ +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Define the local mount point to store local files to +export OUTPUT_DIR="/runtime/output" + +# Directory where all binaries will be loaded +export BIN_DIR="/testrun/bin" + +# Default interface should be veth0 for all containers +export IFACE=veth0 + +# Assign the current host user +export HOST_USER=$(whoami) + +# Create a local user that matches the same as the host +# to be used for correct file ownership for various logs +# HOST_USER mapped in via docker container environment variables +if ! id "$HOST_USER" &>/dev/null; then + useradd "$HOST_USER" +else + echo User $HOST_USER already exists +fi + +# Create the output directory +mkdir -p "$OUTPUT_DIR" + +# Set permissions on the output files +chown -R $HOST_USER $OUTPUT_DIR + +# Enable IPv6 for all containers +sysctl net.ipv6.conf.all.disable_ipv6=0 +sysctl -p + +# Read in the config file +CONF_FILE="/testrun/conf/module_config.json" +CONF=`cat $CONF_FILE` + +if [[ -z $CONF ]] +then + echo "No config file present at $CONF_FILE. Exiting startup." + exit 1 +fi + +# Extract the necessary config parameters +export MODULE_NAME=$(echo "$CONF" | jq -r '.config.meta.name') +export NETWORK_REQUIRED=$(echo "$CONF" | jq -r '.config.network') +export GRPC=$(echo "$CONF" | jq -r '.config.grpc') + +# Validate the module name is present +if [[ -z "$MODULE_NAME" || "$MODULE_NAME" == "null" ]] +then + echo "No module name present in $CONF_FILE. Exiting startup." + exit 1 +fi + +# Setup the PYTHONPATH so all imports work as expected +echo "Setting up PYTHONPATH..." +export PYTHONPATH=$($BIN_DIR/setup_python_path) +echo "PYTHONPATH: $PYTHONPATH" + +echo "Configuring binary files..." +$BIN_DIR/setup_binaries $BIN_DIR + +# Build all gRPC files from the proto for use in +# gRPC clients for communications to network modules +echo "Building gRPC files from available proto files..." +$BIN_DIR/setup_grpc_clients \ No newline at end of file diff --git a/modules/test/base/bin/start b/modules/test/base/bin/start index 37902b868..d1f29989f 100755 --- a/modules/test/base/bin/start +++ b/modules/test/base/bin/start @@ -14,4 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -/testrun/bin/start_module \ No newline at end of file +# Allow one argument which is the unit test file to run +# instead of running the test module +UNIT_TEST_FILE=$1 + +source /testrun/bin/setup + +# Conditionally run start_module based on RUN +if [[ -z "$UNIT_TEST_FILE" ]];then + /testrun/bin/start_module +else + python3 $UNIT_TEST_FILE +fi diff --git a/modules/test/base/bin/start_module b/modules/test/base/bin/start_module index 0ee68fa6a..fb79cc018 100644 --- a/modules/test/base/bin/start_module +++ b/modules/test/base/bin/start_module @@ -1,102 +1,46 @@ -#!/bin/bash - -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Define the local mount point to store local files to -OUTPUT_DIR="/runtime/output" - -# Directory where all binaries will be loaded -BIN_DIR="/testrun/bin" - -# Default interface should be veth0 for all containers -IFACE=veth0 - -# Create a local user that matches the same as the host -# to be used for correct file ownership for various logs -# HOST_USER mapped in via docker container environemnt variables -useradd $HOST_USER - -# Set permissions on the output files -chown -R $HOST_USER $OUTPUT_DIR - -# Enable IPv6 for all containers -sysctl net.ipv6.conf.all.disable_ipv6=0 -sysctl -p - -# Read in the config file -CONF_FILE="/testrun/conf/module_config.json" -CONF=`cat $CONF_FILE` - -if [[ -z $CONF ]] -then - echo "No config file present at $CONF_FILE. Exiting startup." - exit 1 -fi - -# Extract the necessary config parameters -MODULE_NAME=$(echo "$CONF" | jq -r '.config.meta.name') -NETWORK_REQUIRED=$(echo "$CONF" | jq -r '.config.network') -GRPC=$(echo "$CONF" | jq -r '.config.grpc') - -# Validate the module name is present -if [[ -z "$MODULE_NAME" || "$MODULE_NAME" == "null" ]] -then - echo "No module name present in $CONF_FILE. Exiting startup." - exit 1 -fi - -# Setup the PYTHONPATH so all imports work as expected -echo "Setting up PYTHONPATH..." -export PYTHONPATH=$($BIN_DIR/setup_python_path) -echo "PYTHONPATH: $PYTHONPATH" - -# Build all gRPC files from the proto for use in -# gRPC clients for communications to network modules -echo "Building gRPC files from available proto files..." -$BIN_DIR/setup_grpc_clients - -echo "Configuring binary files..." -$BIN_DIR/setup_binaries $BIN_DIR - -echo "Starting module $MODULE_NAME..." - -# Only start network services if the test container needs -# a network connection to run its tests -if [ $NETWORK_REQUIRED == "true" ];then - # Wait for interface to become ready - $BIN_DIR/wait_for_interface $IFACE - - # Start network capture - $BIN_DIR/capture $MODULE_NAME $IFACE -fi - -# Start the grpc server -if [[ ! -z $GRPC && ! $GRPC == "null" ]] -then - GRPC_PORT=$(echo "$GRPC" | jq -r '.port') - if [[ ! -z $GRPC_PORT && ! $GRPC_PORT == "null" ]] - then - echo "gRPC port resolved from config: $GRPC_PORT" - $BIN_DIR/start_grpc "-p $GRPC_PORT" - else - $BIN_DIR/start_grpc - fi -fi - -# Small pause to let all core services stabalize -sleep 3 - -# Start the test module +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +echo "Starting module $MODULE_NAME..." + +# Only start network services if the test container needs +# a network connection to run its tests +if [ $NETWORK_REQUIRED == "true" ];then + # Wait for interface to become ready + $BIN_DIR/wait_for_interface $IFACE + + # Start network capture + $BIN_DIR/capture $MODULE_NAME $IFACE +fi + +# Start the grpc server +if [[ ! -z $GRPC && ! $GRPC == "null" ]] +then + GRPC_PORT=$(echo "$GRPC" | jq -r '.port') + if [[ ! -z $GRPC_PORT && ! $GRPC_PORT == "null" ]] + then + echo "gRPC port resolved from config: $GRPC_PORT" + $BIN_DIR/start_grpc "-p $GRPC_PORT" + else + $BIN_DIR/start_grpc + fi +fi + +# Small pause to let all core services stabalize +sleep 3 + +# Start the test module $BIN_DIR/start_test_module $MODULE_NAME $IFACE \ No newline at end of file diff --git a/modules/test/base/python/requirements.txt b/modules/test/base/python/requirements.txt index 9d9473d74..0a40a660d 100644 --- a/modules/test/base/python/requirements.txt +++ b/modules/test/base/python/requirements.txt @@ -1,3 +1,12 @@ -grpcio -grpcio-tools -netifaces \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +protobuf==5.28.0 + +# User defined packages +grpcio==1.67.1 +grpcio-tools==1.67.1 +netifaces==0.11.0 + +# Requirements for reports generation +Jinja2==3.1.6 diff --git a/modules/test/base/python/src/grpc/proto/host/client.py b/modules/test/base/python/src/grpc/proto/host/client.py new file mode 100644 index 000000000..e08d3376a --- /dev/null +++ b/modules/test/base/python/src/grpc/proto/host/client.py @@ -0,0 +1,63 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License +"""gRPC client module for the secondary DHCP Server""" +import grpc +import host.grpc_pb2_grpc as pb2_grpc +import host.grpc_pb2 as pb2 + +DEFAULT_PORT = '5001' +DEFAULT_HOST = 'external.localhost' # Default DHCP2 server + + +class Client(): + """gRPC Client for the secondary DHCP server""" + def __init__(self, port=DEFAULT_PORT, host=DEFAULT_HOST): + self._port = port + self._host = host + + # Create a gRPC channel to connect to the server + self._channel = grpc.insecure_channel(self._host + ':' + self._port) + + # Create a gRPC stub + self._stub = pb2_grpc.HostNetworkModuleStub(self._channel) + + def check_interface_status(self, iface_name): + # Create a request message + request = pb2.CheckInterfaceStatusRequest() + request.iface_name = iface_name + + # Make the RPC call + response = self._stub.CheckInterfaceStatus(request) + + return response + + def set_iface_down(self, iface_name): + # Create a request message + request = pb2.SetIfaceRequest() + request.iface_name = iface_name + + # Make the RPC call + response = self._stub.SetIfaceDown(request) + + return response + + def set_iface_up(self, iface_name): + # Create a request message + request = pb2.SetIfaceRequest() + request.iface_name = iface_name + + # Make the RPC call + response = self._stub.SetIfaceUp(request) + + return response diff --git a/modules/test/base/python/src/logger.py b/modules/test/base/python/src/logger.py deleted file mode 100644 index e6a2b004c..000000000 --- a/modules/test/base/python/src/logger.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Sets up the logger to be used for the test modules.""" -import json -import logging - -LOGGERS = {} -_LOG_FORMAT = '%(asctime)s %(name)-8s %(levelname)-7s %(message)s' -_DATE_FORMAT = '%b %02d %H:%M:%S' -_DEFAULT_LEVEL = logging.INFO -_CONF_FILE_NAME = 'testrun/system.json' -_LOG_DIR = '/runtime/output/' - -# Set log level -try: - with open(_CONF_FILE_NAME, - encoding='UTF-8') as config_json_file: - system_conf_json = json.load(config_json_file) - - log_level_str = system_conf_json['log_level'] - log_level = logging.getLevelName(log_level_str) -except OSError: - # TODO: Print out warning that log level is incorrect or missing - log_level = _DEFAULT_LEVEL - -log_format = logging.Formatter(fmt=_LOG_FORMAT, datefmt=_DATE_FORMAT) - - -def add_file_handler(log, log_file, log_dir=_LOG_DIR): - handler = logging.FileHandler(log_dir + log_file + '.log') - handler.setFormatter(log_format) - log.addHandler(handler) - - -def add_stream_handler(log): - handler = logging.StreamHandler() - handler.setFormatter(log_format) - log.addHandler(handler) - - -def get_logger(name, log_file=None, log_dir=_LOG_DIR): - if name not in LOGGERS: - LOGGERS[name] = logging.getLogger(name) - LOGGERS[name].setLevel(log_level) - add_stream_handler(LOGGERS[name]) - if log_file is not None: - log_dir = log_dir if log_dir is not None else _LOG_DIR - add_file_handler(LOGGERS[name], log_file, log_dir=log_dir) - return LOGGERS[name] diff --git a/modules/test/base/python/src/test_module.py b/modules/test/base/python/src/test_module.py index 00f74df82..8030d7757 100644 --- a/modules/test/base/python/src/test_module.py +++ b/modules/test/base/python/src/test_module.py @@ -17,6 +17,9 @@ import os import util from datetime import datetime +import traceback + +from common.statuses import TestResult LOGGER = None RESULTS_DIR = '/runtime/output/' @@ -29,7 +32,6 @@ class TestModule: def __init__(self, module_name, log_name, - log_dir=None, conf_file=CONF_FILE, results_dir=RESULTS_DIR): self._module_name = module_name @@ -38,19 +40,22 @@ def __init__(self, self._ipv4_addr = os.environ.get('IPV4_ADDR', '') self._ipv4_subnet = os.environ.get('IPV4_SUBNET', '') self._ipv6_subnet = os.environ.get('IPV6_SUBNET', '') - self._add_logger(log_name=log_name, - module_name=module_name, - log_dir=log_dir) + self._dev_iface_mac = os.environ.get('DEV_IFACE_MAC', '') + self._device_test_pack = json.loads(os.environ.get('DEVICE_TEST_PACK', '')) + self._report_template_folder = os.environ.get('REPORT_TEMPLATE_PATH') + self._base_template_file=os.environ.get('BASE_TEMPLATE_FILE') + self._log_level = os.environ.get('LOG_LEVEL', None) + self._add_logger(log_name=log_name) self._config = self._read_config( conf_file=conf_file if conf_file is not None else CONF_FILE) self._device_ipv4_addr = None self._device_ipv6_addr = None - def _add_logger(self, log_name, module_name, log_dir=None): + def _add_logger(self, log_name): global LOGGER - LOGGER = logger.get_logger(name=log_name, - log_file=module_name, - log_dir=log_dir) # pylint: disable=E1123 + LOGGER = logger.get_logger(name=log_name) + if self._log_level is not None: + LOGGER.setLevel(self._log_level) def generate_module_report(self): pass @@ -64,22 +69,48 @@ def _get_tests(self): def _get_device_tests(self, device_test_module): module_tests = self._config['config']['tests'] - if device_test_module is None: - return module_tests - elif not device_test_module['enabled']: - return [] - else: - for test in module_tests: - # Resolve device specific configurations for the test if it exists - # and update module test config with device config options - if 'tests' in device_test_module: - if test['name'] in device_test_module['tests']: - dev_test_config = device_test_module['tests'][test['name']] - if 'enabled' in dev_test_config: - test['enabled'] = dev_test_config['enabled'] - if 'config' in test and 'config' in dev_test_config: - test['config'].update(dev_test_config['config']) - return module_tests + tests_to_run = module_tests + + # If no device specific tests have been provided, add all + if device_test_module is not None: + # Do not run any tests if module is disabled for this device + if not device_test_module['enabled']: + return [] + + # Tests that will be removed because they are not in the test pack + remove_tests = [] + + # Check if all tests are in the test pack and enabled for the device + for test in tests_to_run: + + # Resolve device specific configurations for the test if it exists + # and update module test config with device config options + if 'tests' in device_test_module: + + if test['name'] in device_test_module['tests']: + dev_test_config = device_test_module['tests'][test['name']] + + # Check if the test is enabled in the device config + if 'enabled' in dev_test_config: + test['enabled'] = dev_test_config['enabled'] + + # Copy over any device specific test configuration + if 'config' in test and 'config' in dev_test_config: + test['config'].update(dev_test_config['config']) + + # Search for the module test in the test pack + found = False + for test_pack_test in self._device_test_pack['tests']: + if test_pack_test['name'] == test['name']: + # Test is in the test pack + found = True + + if not found: + remove_tests.append(test) + for test in remove_tests: + tests_to_run.remove(test) + + return tests_to_run def _get_device_test_module(self): if 'DEVICE_TEST_MODULES' in os.environ: @@ -111,37 +142,44 @@ def run_tests(self): else: result = getattr(self, test_method_name)() except Exception as e: # pylint: disable=W0718 - LOGGER.error(f'An error occurred whilst running {test["name"]}') + LOGGER.error(f'An error occurred whilst running {test["name"]}') # pylint: disable=W1405 LOGGER.error(e) + traceback.print_exc() else: - LOGGER.info(f'Test {test["name"]} not implemented. Skipping') + LOGGER.error(f'Test {test["name"]} has not been implemented') # pylint: disable=W1405 + result = TestResult.ERROR, 'This test could not be found' else: - LOGGER.debug(f'Test {test["name"]} is disabled') + LOGGER.debug(f'Test {test["name"]} is disabled') # pylint: disable=W1405 + result = (TestResult.DISABLED, + 'This test did not run because it is disabled') + # Check if the test module has returned a result if result is not None: + # Compliant or non-compliant as a boolean only if isinstance(result, bool): - test['result'] = 'Compliant' if result else 'Non-Compliant' + test['result'] = (TestResult.COMPLIANT + if result else TestResult.NON_COMPLIANT) test['description'] = 'No description was provided for this test' else: - # TODO: This is assuming that result is an array but haven't checked # Error result if result[0] is None: - test['result'] = 'Error' + test['result'] = TestResult.ERROR if len(result) > 1: test['description'] = result[1] else: - test['description'] = 'An error occured whilst running this test' + test['description'] = 'An error occurred whilst running this test' # Compliant / Non-Compliant result elif isinstance(result[0], bool): - test['result'] = 'Compliant' if result[0] else 'Non-Compliant' + test['result'] = (TestResult.COMPLIANT + if result[0] else TestResult.NON_COMPLIANT) # Result may be a string, e.g Error, Feature Not Detected elif isinstance(result[0], str): test['result'] = result[0] else: LOGGER.error(f'Unknown result detected: {result[0]}') - test['result'] = 'Error' + test['result'] = TestResult.ERROR # Check that description is a string if isinstance(result[1], str): @@ -152,12 +190,17 @@ def run_tests(self): # Check if details were provided if len(result)>2: test['details'] = result[2] + + # Check if tags were provided + if len(result)>3: + test['tags'] = result[3] else: - test['result'] = 'Error' - test['description'] = 'An error occured whilst running this test' + LOGGER.debug('No result was returned from the test module') + test['result'] = TestResult.ERROR + test['description'] = 'An error occurred whilst running this test' # Remove the steps to resolve if compliant already - if (test['result'] == 'Compliant' and 'recommendations' in test): + if (test['result'] == TestResult.COMPLIANT and 'recommendations' in test): test.pop('recommendations') test['end'] = datetime.now().isoformat() @@ -182,7 +225,7 @@ def _write_results(self, results): def _get_device_ipv4(self): command = f"""/testrun/bin/get_ipv4_addr {self._ipv4_subnet} {self._device_mac.upper()}""" - text = util.run_command(command)[0] + text = util.run_command(command)[0] # pylint: disable=E1120 if text: return text.split('\n')[0] return None diff --git a/modules/test/base/python/src/util.py b/modules/test/base/python/src/util.py deleted file mode 100644 index 006648037..000000000 --- a/modules/test/base/python/src/util.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Provides basic utilities for a test module.""" -import subprocess -import shlex -import logger - -LOGGER = logger.get_logger('util') - -def run_command(cmd, output=True): - """Runs a process at the os level - By default, returns the standard output and error output - If the caller sets optional output parameter to False, - will only return a boolean result indicating if it was - successful in running the command. Failure is indicated - by any return code from the process other than zero.""" - - success = False - with subprocess.Popen(shlex.split(cmd), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as process: - stdout, stderr = process.communicate() - if process.returncode != 0 and output: - err_msg = f'{stderr.strip()}. Code: {process.returncode}' - LOGGER.error('Command Failed: ' + cmd) - LOGGER.error('Error: ' + err_msg) - else: - success = True - LOGGER.debug('Command succeeded: ' + cmd) - if output: - out = stdout.strip().decode('utf-8') - LOGGER.debug('Command output: ' + out) - return out, stderr - else: - return success diff --git a/modules/test/baseline/baseline.Dockerfile b/modules/test/baseline/baseline.Dockerfile index f7d21f8c8..7a83c8de2 100644 --- a/modules/test/baseline/baseline.Dockerfile +++ b/modules/test/baseline/baseline.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/baseline-test -FROM test-run/base-test:latest +# Image name: testrun/baseline-test +FROM testrun/base-test:latest ARG MODULE_NAME=baseline ARG MODULE_DIR=modules/test/$MODULE_NAME diff --git a/modules/test/baseline/bin/start_test_module b/modules/test/baseline/bin/start_test_module index a529c2fcf..c3209261a 100644 --- a/modules/test/baseline/bin/start_test_module +++ b/modules/test/baseline/bin/start_test_module @@ -41,11 +41,8 @@ else fi # Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python scrip that will execute the tests for this module diff --git a/modules/test/baseline/conf/module_config.json b/modules/test/baseline/conf/module_config.json index f28f74a06..d120f1c71 100644 --- a/modules/test/baseline/conf/module_config.json +++ b/modules/test/baseline/conf/module_config.json @@ -16,20 +16,17 @@ { "name": "baseline.compliant", "test_description": "Simulate a compliant test", - "expected_behavior": "A compliant test result is generated", - "required_result": "Required" + "expected_behavior": "A compliant test result is generated" }, { "name": "baseline.non_compliant", "test_description": "Simulate a non-compliant test", - "expected_behavior": "A non-compliant test result is generated", - "required_result": "Recommended" + "expected_behavior": "A non-compliant test result is generated" }, { "name": "baseline.skipped", "test_description": "Simulate a skipped test", - "expected_behavior": "A skipped test result is generated", - "required_result": "Skipped" + "expected_behavior": "A skipped test result is generated" } ] } diff --git a/modules/test/conn/README.md b/modules/test/conn/README.md index c2f6377c6..10c7e6917 100644 --- a/modules/test/conn/README.md +++ b/modules/test/conn/README.md @@ -17,11 +17,15 @@ Within the ```python/src``` directory, the below tests are executed. A few dhcp | connection.port_link | The network switch port connected to the device has an active link without errors | When the etherent cable is connected to the port, the device triggers the port to its enabled \"Link UP\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \"show interface\" command on most network switches. | Required | | connection.port_speed | The network switch port connected to the device has auto-negotiated a speed that is 10 Mbps or higher | When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \"show interface\" command on most network switches. The output of this command must also show that the \"configured speed\" is set to \"auto\". | Required | | connection.port_duplex | The network switch port connected to the device has auto-negotiated full-duplex. | When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection. | Required | +| connection.switch.arp_inspection | The device implements ARP correctly as per RFC826 | Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled. | Required | +| connection.switch.dhcp_snooping | The device operates as a DHCP client and operates correctly when DHCP snooping is enabled on a switch. | Device continues to operate correctly when DHCP snooping is enabled on the switch. | Required | | connection.dhcp_address | The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request | The device is not set up with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds successfully to an ICMP echo (ping) request. | Required | | connection.mac_address | Check and note device physical address. | N/A | Required | | connection.mac_oui | The device under test has a MAC address prefix that is registered against a known manufacturer. | The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database. | Required | | connection.private_address | The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets. | The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets: 10.0.0.0 - 10.255.255.255 (10/8 prefix), 172.16.0.0 - 172.31.255.255 (172.16/12 prefix), 192.168.0.0 - 192.168.255.255 (192.168/16 prefix). | Required | | connection.shared_address | Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space | The device under test accepts IP addresses within the range specified in RFC 6598 and communicates using these addresses. | Required | +| connection.dhcp_disconnect | The device under test issues a new DHCPREQUEST packet after a port ph ysical disconnection and reconnection. | A client SHOULD use DHCP to reacquire or verify its IP address and network parameters whenever the local network parameters may have changed; e.g., at system boot time or after a disconnection from the local network, as the local network configuration may change without the client's or user's knowledge. If a client has knowledge ofa previous network address and is unable to contact a local DHCP server, the client may continue to use the previous network addres until the lease for that address expires. If the lease expires before the client can contact a DHCP server, the client must immediately discontinue use of the previous network address and may inform local users of the problem. | Required | +| connection.dhcp_disconnect_ip_change | When device is disconnected, update device IP on the DHCP server and reconnect the device. Ensure device received new IP address. | If IP address for a device was changed on the DHCP server while the device was disconnected then the device should request and update the new IP upon reconnecting to the network | Required | | connection.single_ip | The network switch port connected to the device reports only one IP address for the device under test. | The device under test does not behave as a network switch and only requests one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy-chained devices to one single network port, as this would not make 802.1x port-based authentication possible. | Required | | connection.target_ping | The device under test responds to an ICMP echo (ping) request. | The device under test responds to an ICMP echo (ping) request. | Required | | connection.ipaddr.ip_change | The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired. | If the lease expires before the client receives a DHCPACK, the client moves to the INIT state, MUST immediately stop any other network processing, and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network address, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem. | Required | diff --git a/modules/test/conn/bin/get_packet_counts.sh b/modules/test/conn/bin/get_packet_counts.sh new file mode 100644 index 000000000..fab933a08 --- /dev/null +++ b/modules/test/conn/bin/get_packet_counts.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Check if MAC address and pcap file arguments are provided +if [ -z "$1" ] || [ -z "$2" ]; then + echo "Usage: $0 " + exit 1 +fi + +# Assign MAC address and pcap file from arguments +PCAP_FILE="$1" +MAC_ADDRESS="$2" + +# Check if the pcap file exists +if [ ! -f "$PCAP_FILE" ]; then + echo "Error: File $PCAP_FILE does not exist." + exit 1 +fi + +# Count multicast packets from the MAC address +multicast_from_count=$(tshark -r "$PCAP_FILE" -Y "(eth.dst[0] == 1) && eth.src == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Count multicast packets to the MAC address +multicast_to_count=$(tshark -r "$PCAP_FILE" -Y "(eth.dst[0] == 1) && eth.dst == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Count broadcast packets from the MAC address (broadcast MAC address is FF:FF:FF:FF:FF:FF) +broadcast_from_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst == ff:ff:ff:ff:ff:ff && eth.src == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Count broadcast packets to the MAC address +broadcast_to_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst == ff:ff:ff:ff:ff:ff && eth.dst == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Count unicast packets from the MAC address +unicast_from_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst != ff:ff:ff:ff:ff:ff && (eth.dst[0] & 1) == 0 && eth.src == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Count unicast packets to the MAC address +unicast_to_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst != ff:ff:ff:ff:ff:ff && (eth.dst[0] & 1) == 0 && eth.dst == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Output the results as a JSON object +echo "{ + \"mac_address\": \"$MAC_ADDRESS\", + \"multicast\": { + \"from\": $multicast_from_count, + \"to\": $multicast_to_count + }, + \"broadcast\": { + \"from\": $broadcast_from_count, + \"to\": $broadcast_to_count + }, + \"unicast\": { + \"from\": $unicast_from_count, + \"to\": $unicast_to_count + } +}" diff --git a/modules/test/conn/bin/start_test_module b/modules/test/conn/bin/start_test_module index d85ae7d6b..4cdba0ae9 100644 --- a/modules/test/conn/bin/start_test_module +++ b/modules/test/conn/bin/start_test_module @@ -40,9 +40,7 @@ fi # Create and set permissions on the log files LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python script that will execute the tests for this module diff --git a/modules/test/conn/conf/module_config.json b/modules/test/conn/conf/module_config.json index 5289e7eb0..03ae51d89 100644 --- a/modules/test/conn/conf/module_config.json +++ b/modules/test/conn/conf/module_config.json @@ -16,48 +16,42 @@ { "name": "connection.port_link", "test_description": "The network switch port connected to the device has an active link without errors", - "expected_behavior": "When the etherent cable is connected to the port, the device triggers the port to its enabled \"Link UP\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \"show interface\" command on most network switches.", - "required_result": "Required" + "expected_behavior": "When the ethernet cable is connected to the port, the device triggers the port to its enabled \"Link UP\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \"show interface\" command on most network switches." }, { "name": "connection.port_speed", "test_description": "The network switch port connected to the device has auto-negotiated a speed that is 10 Mbps or higher", - "expected_behavior": "When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \"show interface\" command on most network switches. The output of this command must also show that the \"configured speed\" is set to \"auto\".", - "required_result": "Required" + "expected_behavior": "When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \"show interface\" command on most network switches. The output of this command must also show that the \"configured speed\" is set to \"auto\"." }, { "name": "connection.port_duplex", "test_description": "The network switch port connected to the device has auto-negotiated full-duplex", - "expected_behavior": "When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection.", - "required_result": "Required" + "expected_behavior": "When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection." }, { "name": "connection.switch.arp_inspection", "test_description": "The device implements ARP correctly as per RFC826", - "expected_behavior": "Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled.", - "required_result": "Required" + "expected_behavior": "Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled." }, { "name": "connection.switch.dhcp_snooping", "test_description": "The device operates as a DHCP client and operates correctly when DHCP snooping is enabled on a switch.", - "expected_behavior": "Device continues to operate correctly when DHCP snooping is enabled on the switch.", - "required_result": "Required" + "expected_behavior": "Device continues to operate correctly when DHCP snooping is enabled on the switch." }, { "name": "connection.dhcp_address", "test_description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request", - "expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.", - "required_result": "Required", + "expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds successfully to an ICMP echo (ping) request.", "recommendations": [ "Enable DHCP", - "Install a DHCP client" + "Install a DHCP client", + "Ensure that your DHCP client renews its lease at the correct time" ] }, { "name": "connection.mac_address", "test_description": "Check and note device physical address.", "expected_behavior": "N/A", - "required_result": "Required", "recommendations": [ "Ensure that the MAC address is set by hardware only" ] @@ -66,7 +60,6 @@ "name": "connection.mac_oui", "test_description": "The device under test has a MAC address prefix that is registered against a known manufacturer.", "expected_behavior": "The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database.", - "required_result": "Required", "recommendations": [ "Register the device MAC address with IEEE" ] @@ -75,7 +68,6 @@ "name": "connection.private_address", "test_description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.", "expected_behavior": "The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)", - "required_result": "Required", "config": { "lease_wait_time_sec": 60, "ranges": [ @@ -101,7 +93,6 @@ "name": "connection.shared_address", "test_description": "Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space", "expected_behavior": "The device under test accepts IP addresses within the ranges specified in RFC 6598 and communicates using these addresses", - "required_result": "Required", "config": { "lease_wait_time_sec": 60, "ranges": [ @@ -116,11 +107,20 @@ "Enable shared address space support in the DHCP client" ] }, + { + "name": "connection.dhcp_disconnect", + "test_description": "The device under test issues a new DHCPREQUEST packet after a port physical disconnection and reconnection", + "expected_behavior": "A client SHOULD use DHCP to reacquire or verify its IP address and network parameters whenever the local network parameters may have changed; e.g., at system boot time or after a disconnection from the local network, as the local network configuration may change without the client's or user's knowledge. If a client has knowledge ofa previous network address and is unable to contact a local DHCP server, the client may continue to use the previous network address until the lease for that address expires. If the lease expires before the client can contact a DHCP server, the client must immediately discontinue use of the previous network address and may inform local users of the problem." + }, + { + "name": "connection.dhcp_disconnect_ip_change", + "test_description": "When device is disconnected, update device IP on the DHCP server and reconnect the device. Ensure device received new IP address", + "expected_behavior": "If IP address for a device was changed on the DHCP server while the device was disconnected then the device should request and update the new IP upon reconnecting to the network" + }, { "name": "connection.single_ip", "test_description": "The network switch port connected to the device reports only one IP address for the device under test.", - "expected_behavior": "The device under test does not behave as a network switch and only requets one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.", - "required_result": "Required", + "expected_behavior": "The device under test does not behave as a network switch and only requests one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.", "recommendations": [ "Ensure that all ports on the device are isolated", "Ensure only one DHCP client is running" @@ -130,7 +130,6 @@ "name": "connection.target_ping", "test_description": "The device under test responds to an ICMP echo (ping) request.", "expected_behavior": "The device under test responds to an ICMP echo (ping) request.", - "required_result": "Required", "recommendations": [ "Configure device to allow ICMP requests (ping)", "Create a firewall exception to allow ICMP requests from LAN" @@ -139,8 +138,7 @@ { "name": "connection.ipaddr.ip_change", "test_description": "The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired.", - "expected_behavior": "If the lease expires before the client receiveds a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network addres, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.", - "required_result": "Required", + "expected_behavior": "If the lease expires before the client receives a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network address, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.", "config":{ "lease_wait_time_sec": 60 }, @@ -152,7 +150,6 @@ "name": "connection.ipaddr.dhcp_failover", "test_description": "The device has requested a DHCPREQUEST/REBIND to the DHCP failover server after the primary DHCP server has been brought down.", "expected_behavior": "", - "required_result": "Required", "config":{ "lease_wait_time_sec": 60 }, @@ -164,7 +161,6 @@ "name": "connection.ipv6_slaac", "test_description": "The device forms a valid IPv6 address as a combination of the IPv6 router prefix and the device interface identifier", "expected_behavior": "The device under test complies with RFC4862 and forms a valid IPv6 SLAAC address", - "required_result": "Required", "recommendations": [ "Install a network manager that supports IPv6", "Disable DHCPv6" @@ -174,11 +170,15 @@ "name": "connection.ipv6_ping", "test_description": "The device responds to an IPv6 ping (ICMPv6 Echo) request to the SLAAC address", "expected_behavior": "The device responds to the ping as per RFC4443", - "required_result": "Required", "recommendations": [ "Enable ping response to IPv6 ICMP requests in network manager settings", "Create a firewall exception to allow ICMPv6 via LAN" ] + }, + { + "name": "communication.network.type", + "test_description": "How does the device communicate (flow type) - Unicast, multicast broadcast?", + "expected_behavior": "Informational - One or more of these flow types are used" } ] } diff --git a/modules/test/conn/conn.Dockerfile b/modules/test/conn/conn.Dockerfile index a9f523e44..cda0858c9 100644 --- a/modules/test/conn/conn.Dockerfile +++ b/modules/test/conn/conn.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/conn-test -FROM test-run/base-test:latest +# Image name: testrun/conn-test +FROM testrun/base-test:latest ARG MODULE_NAME=conn ARG MODULE_DIR=modules/test/$MODULE_NAME @@ -21,13 +21,13 @@ ARG GRPC_PROTO_DIR=/testrun/python/src/grpc/proto/dhcp ARG GRPC_PROTO_FILE="grpc.proto" # Install all necessary packages -RUN apt-get install -y wget +RUN apt-get install -y wget tshark # Load the requirements file COPY $MODULE_DIR/python/requirements.txt /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf @@ -35,5 +35,11 @@ COPY $MODULE_DIR/conf /testrun/conf # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin +# Remove incorrect line endings +RUN dos2unix /testrun/bin/* + +# Make sure all the bin files are executable +RUN chmod u+x /testrun/bin/* + # Copy over all python files COPY $MODULE_DIR/python /testrun/python \ No newline at end of file diff --git a/modules/test/conn/python/requirements.txt b/modules/test/conn/python/requirements.txt index 7244e9e75..d0f5db19a 100644 --- a/modules/test/conn/python/requirements.txt +++ b/modules/test/conn/python/requirements.txt @@ -1,3 +1,12 @@ -pyOpenSSL -scapy -python-dateutil \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +cffi==1.17.1 +cryptography==44.0.1 +pycparser==2.22 +six==1.16.0 + +# User defined packages +pyOpenSSL==24.3.0 +scapy==2.6.0 +python-dateutil==2.9.0.post0 diff --git a/modules/test/conn/python/src/connection_module.py b/modules/test/conn/python/src/connection_module.py index 5e8b78ec3..fdb1ae5cc 100644 --- a/modules/test/conn/python/src/connection_module.py +++ b/modules/test/conn/python/src/connection_module.py @@ -15,15 +15,20 @@ import util import time import traceback -from scapy.all import rdpcap, DHCP, ARP, Ether, IPv6, ICMPv6ND_NS +import os +from scapy.error import Scapy_Exception +from scapy.all import rdpcap, DHCP, ARP, Ether, ICMP, IPv6, ICMPv6ND_NS from test_module import TestModule from dhcp1.client import Client as DHCPClient1 from dhcp2.client import Client as DHCPClient2 +from host.client import Client as HostClient from dhcp_util import DHCPUtil from port_stats_util import PortStatsUtil +import json LOG_NAME = 'test_connection' OUI_FILE = '/usr/local/etc/oui.txt' +DEFAULT_BIN_DIR = '/testrun/bin' STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap' MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap' DHCP_CAPTURE_FILE = '/runtime/network/dhcp-1.pcap' @@ -39,19 +44,29 @@ class ConnectionModule(TestModule): """Connection Test module""" - def __init__(self, module, log_dir=None, conf_file=None, results_dir=None): + def __init__(self, + module, + conf_file=None, + results_dir=None, + startup_capture_file=STARTUP_CAPTURE_FILE, + monitor_capture_file=MONITOR_CAPTURE_FILE, + bin_dir=DEFAULT_BIN_DIR): + super().__init__(module_name=module, log_name=LOG_NAME, - log_dir=log_dir, conf_file=conf_file, results_dir=results_dir) global LOGGER LOGGER = self._get_logger() + self.startup_capture_file = startup_capture_file + self.monitor_capture_file = monitor_capture_file self._port_stats = PortStatsUtil(logger=LOGGER) self.dhcp1_client = DHCPClient1() self.dhcp2_client = DHCPClient2() + self.host_client = HostClient() self._dhcp_util = DHCPUtil(self.dhcp1_client, self.dhcp2_client, LOGGER) self._lease_wait_time_sec = LEASE_WAIT_TIME_DEFAULT + self._bin_dir = bin_dir # ToDo: Move this into some level of testing, leave for # reference until tests are implemented with these calls @@ -106,7 +121,8 @@ def _connection_switch_arp_inspection(self): no_arp = True # Read all the pcap files - packets = rdpcap(STARTUP_CAPTURE_FILE) + rdpcap(MONITOR_CAPTURE_FILE) + packets = rdpcap(self.startup_capture_file) + rdpcap( + self.monitor_capture_file) for packet in packets: # We are not interested in packets unless they are ARP packets @@ -123,12 +139,8 @@ def _connection_switch_arp_inspection(self): # Check MAC address matches IP address if (arp_packet.hwsrc == self._device_mac - and (arp_packet.psrc not in ( - self._device_ipv4_addr, - '0.0.0.0' - )) and not arp_packet.psrc.startswith( - '169.254' - )): + and (arp_packet.psrc not in (self._device_ipv4_addr, '0.0.0.0')) + and not arp_packet.psrc.startswith('169.254')): LOGGER.info(f'Bad ARP packet detected for MAC: {self._device_mac}') LOGGER.info(f'''ARP packet from IP {arp_packet.psrc} does not match {self._device_ipv4_addr}''') @@ -137,7 +149,7 @@ def _connection_switch_arp_inspection(self): if no_arp: return None, 'No ARP packets from the device found' - return True, 'Device uses ARP' + return True, 'Device uses ARP correctly' def _connection_switch_dhcp_snooping(self): LOGGER.info('Running connection.switch.dhcp_snooping') @@ -145,7 +157,8 @@ def _connection_switch_dhcp_snooping(self): disallowed_dhcp_types = [2, 4, 5, 6, 9, 10, 12, 13, 15, 17] # Read all the pcap files - packets = rdpcap(STARTUP_CAPTURE_FILE) + rdpcap(MONITOR_CAPTURE_FILE) + packets = rdpcap(self.startup_capture_file) + rdpcap( + self.monitor_capture_file) for packet in packets: # We are not interested in packets unless they are DHCP packets @@ -158,6 +171,11 @@ def _connection_switch_dhcp_snooping(self): dhcp_type = self._get_dhcp_type(packet) if dhcp_type in disallowed_dhcp_types: + + # Check if packet is responding with port unreachable + if ICMP in packet and packet[ICMP].type == 3: + continue + return False, 'Device has sent disallowed DHCP message' return True, 'Device does not act as a DHCP server' @@ -182,15 +200,17 @@ def _connection_dhcp_address(self): ping_success = self._ping(self._device_ipv4_addr) LOGGER.debug('Ping success: ' + str(ping_success)) if ping_success: - return True, 'Device responded to leased ip address' + return True, 'Device responded to leased IP address' else: - return False, 'Device did not respond to leased ip address' + return False, 'Device did not respond to leased IP address' else: LOGGER.info('No IP information found in lease: ' + self._device_mac) return False, 'No IP information found in lease: ' + self._device_mac else: - LOGGER.info('No DHCP lease could be found: ' + self._device_mac) - return False, 'No DHCP lease could be found: ' + self._device_mac + LOGGER.info('No DHCP lease could be found for MAC ' + self._device_mac + + ' at the time of this test') + return (False, 'No DHCP lease could be found for MAC ' + + self._device_mac + ' at the time of this test') def _connection_mac_address(self): LOGGER.info('Running connection.mac_address') @@ -220,7 +240,8 @@ def _connection_single_ip(self): return result, 'No MAC address found.' # Read all the pcap files containing DHCP packet information - packets = rdpcap(STARTUP_CAPTURE_FILE) + rdpcap(MONITOR_CAPTURE_FILE) + packets = rdpcap(self.startup_capture_file) + rdpcap( + self.monitor_capture_file) # Extract MAC addresses from DHCP packets mac_addresses = set() @@ -230,7 +251,8 @@ def _connection_single_ip(self): if self._get_dhcp_type(packet) == 3: mac_address = packet[Ether].src LOGGER.info('DHCPREQUEST detected MAC address: ' + mac_address) - if not mac_address.startswith(TR_CONTAINER_MAC_PREFIX): + if (not mac_address.startswith(TR_CONTAINER_MAC_PREFIX) + and mac_address != self._dev_iface_mac): mac_addresses.add(mac_address.upper()) # Check if the device mac address is in the list of DHCPREQUESTs @@ -289,7 +311,7 @@ def _connection_ipaddr_ip_change(self, config): lease['hw_addr'], ip_address): self._dhcp_util.wait_for_lease_expire(lease, self._lease_wait_time_sec) - LOGGER.info('Checking device accepted new ip') + LOGGER.info('Checking device accepted new IP') for _ in range(5): LOGGER.info('Pinging device at IP: ' + ip_address) if self._ping(ip_address): @@ -306,8 +328,10 @@ def _connection_ipaddr_ip_change(self, config): else: result = None, 'Failed to create reserved lease for device' else: - LOGGER.info('Device has no current DHCP lease') - result = None, 'Device has no current DHCP lease' + LOGGER.info('Device has no current DHCP lease so ' + + 'this test could not be run') + result = None, ('Device has no current DHCP lease so ' + + 'this test could not be run') # Restore the network self._dhcp_util.restore_failover_dhcp_server() LOGGER.info('Waiting 30 seconds for reserved lease to expire') @@ -360,12 +384,198 @@ def _connection_ipaddr_dhcp_failover(self, config): else: result = False, 'Device did not respond to ping' else: - result = None, 'Device has no current DHCP lease' + result = ( + None, + 'Device has no current DHCP lease so this test could not be run') else: LOGGER.error('Network is not ready for this test. Skipping') result = None, 'Network is not ready for this test' return result + def _connection_dhcp_disconnect(self): + LOGGER.info('Running connection.dhcp.disconnect') + result = None + description = '' + dev_iface = os.getenv('DEV_IFACE') + + try: + iface_status = self.host_client.check_interface_status(dev_iface) + if iface_status.code == 200: + LOGGER.info('Successfully resolved iface status') + if iface_status.status: + lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, + timeout=self._lease_wait_time_sec) + if lease is not None: + LOGGER.info('Current device lease resolved') + if self._dhcp_util.is_lease_active(lease): + + # Disable the device interface + iface_down = self.host_client.set_iface_down(dev_iface) + if iface_down: + LOGGER.info('Device interface set to down state') + + # Wait for the lease to expire + self._dhcp_util.wait_for_lease_expire(lease, + self._lease_wait_time_sec) + + # Wait an additonal 10 seconds to better test a true disconnect + # state + LOGGER.info('Waiting 10 seconds before bringing iface back up') + time.sleep(10) + + # Enable the device interface + iface_up = self.host_client.set_iface_up(dev_iface) + if iface_up: + LOGGER.info('Device interface set to up state') + + # Confirm device receives a new lease + if self._dhcp_util.get_cur_lease( + mac_address=self._device_mac, + timeout=self._lease_wait_time_sec): + if self._dhcp_util.is_lease_active(lease): + result = True + description = ( + 'Device received a DHCP lease after disconnect') + else: + result = False + description = ( + 'Could not confirm DHCP lease active after disconnect') + else: + result = False + description = ( + 'Device did not recieve a DHCP lease after disconnect') + else: + result = 'Error' + description = 'Failed to set device interface to up state' + else: + result = 'Error' + description = 'Failed to set device interface to down state' + else: + result = 'Error' + description = 'No active lease available for device' + else: + result = 'Error' + description = 'Device interface is down' + else: + result = 'Error' + description = 'Device interface could not be resolved' + + except Exception: + LOGGER.error('Unable to connect to gRPC server') + result = 'Error' + description = ( + 'Unable to connect to gRPC server' + ) + return result, description + + def _connection_dhcp_disconnect_ip_change(self): + LOGGER.info('Running connection.dhcp.disconnect_ip_change') + result = None + description = '' + reserved_lease = None + dev_iface = os.getenv('DEV_IFACE') + if self._dhcp_util.setup_single_dhcp_server(): + try: + iface_status = self.host_client.check_interface_status(dev_iface) + if iface_status.code == 200: + LOGGER.info('Successfully resolved iface status') + if iface_status.status: + lease = self._dhcp_util.get_cur_lease( + mac_address=self._device_mac, timeout=self._lease_wait_time_sec) + if lease is not None: + LOGGER.info('Current device lease resolved') + if self._dhcp_util.is_lease_active(lease): + + # Add a reserved lease with a different IP + ip_address = '10.10.10.30' + reserved_lease = self._dhcp_util.add_reserved_lease( + lease['hostname'], self._device_mac, ip_address) + + # Disable the device interface + iface_down = self.host_client.set_iface_down(dev_iface) + if iface_down: + LOGGER.info('Device interface set to down state') + + # Wait for the lease to expire + self._dhcp_util.wait_for_lease_expire(lease, + self._lease_wait_time_sec) + + if reserved_lease: + # Wait an additonal 10 seconds to better test a true + # disconnect state + LOGGER.info( + 'Waiting 10 seconds before bringing iface back up') + time.sleep(10) + + # Enable the device interface + iface_up = self.host_client.set_iface_up(dev_iface) + if iface_up: + LOGGER.info('Device interface set to up state') + # Confirm device receives a new lease + reserved_lease_accepted = False + LOGGER.info('Checking device accepted new IP') + for _ in range(5): + LOGGER.info('Pinging device at IP: ' + ip_address) + if self._ping(ip_address): + LOGGER.debug('Ping success') + LOGGER.debug( + 'Reserved lease confirmed active in device') + reserved_lease_accepted = True + break + else: + LOGGER.info('Device did not respond to ping') + time.sleep(5) # Wait 5 seconds before trying again + + if reserved_lease_accepted: + result = True + description = ('Device received expected IP address ' + 'after disconnect') + else: + result = False + description = ( + 'Could not confirm DHCP lease active after disconnect' + ) + else: + result = 'Error' + description = 'Failed to set device interface to up state' + else: + result = 'Error' + description = ( + 'Failed to set reserved address in DHCP server' + ) + else: + result = 'Error' + description = 'Failed to set device interface to down state' + else: + result = 'Error' + description = 'No active lease available for device' + else: + result = 'Error' + description = 'Device interface is down' + else: + result = 'Error' + description = 'Device interface could not be resolved' + except Exception: + LOGGER.error('Unable to connect to gRPC server') + result = 'Error' + description = ( + 'Unable to connect to gRPC server' + ) + else: + result = 'Error' + description = 'Failed to configure network for test' + + if reserved_lease: + self._dhcp_util.delete_reserved_lease(self._device_mac) + + # Restore the network + self._dhcp_util.restore_failover_dhcp_server() + LOGGER.info('Waiting 30 seconds for reserved lease to expire') + time.sleep(30) + self._dhcp_util.get_cur_lease(mac_address=self._device_mac, + timeout=self._lease_wait_time_sec) + return result, description + def _get_oui_manufacturer(self, mac_address): # Do some quick fixes on the format of the mac_address # to match the oui file pattern @@ -381,10 +591,12 @@ def _connection_ipv6_slaac(self): LOGGER.info('Running connection.ipv6_slaac') result = None - slac_test, sends_ipv6 = self._has_slaac_addres() + slac_test, sends_ipv6 = self._has_slaac_address() if slac_test: result = True, f'Device has formed SLAAC address {self._device_ipv6_addr}' - if result is None: + elif slac_test is None: + result = 'Error', 'An error occurred whilst running this test' + else: if sends_ipv6: LOGGER.info('Device does not support IPv6 SLAAC') result = False, 'Device does not support IPv6 SLAAC' @@ -393,9 +605,15 @@ def _connection_ipv6_slaac(self): result = False, 'Device does not support IPv6' return result - def _has_slaac_addres(self): - packet_capture = (rdpcap(STARTUP_CAPTURE_FILE) + - rdpcap(MONITOR_CAPTURE_FILE) + rdpcap(DHCP_CAPTURE_FILE)) + def _has_slaac_address(self): + packet_capture = (rdpcap(self.startup_capture_file) + + rdpcap(self.monitor_capture_file)) + + try: + packet_capture += rdpcap(DHCP_CAPTURE_FILE) + except (FileNotFoundError, Scapy_Exception): + LOGGER.error('dhcp-1.pcap not found or empty, ignoring') + sends_ipv6 = False for packet_number, packet in enumerate(packet_capture, start=1): if IPv6 in packet and packet.src == self._device_mac: @@ -432,7 +650,7 @@ def _ping(self, host, ipv6=False): cmd += ' -6 ' if ipv6 else '' cmd += str(host) #cmd = 'ping -c 1 ' + str(host) - success = util.run_command(cmd, output=False) + success = util.run_command(cmd, output=False) # pylint: disable=E1120 return success def restore_failover_dhcp_server(self, subnet): @@ -478,6 +696,67 @@ def setup_single_dhcp_server(self): else: return False, 'Secondary DHCP server stop command failed' + def _communication_network_type(self): + try: + result = 'Informational' + description = '' + details = '' + packets = self.get_network_packet_types() + details = packets + # Initialize a list for detected packet types + packet_types = [] + + # Check for the presence of each packet type and append to the list + if (packets['multicast']['from'] > 0) or (packets['multicast']['to'] > 0): + packet_types.append('Multicast') + if (packets['broadcast']['from'] > 0) or (packets['broadcast']['to'] > 0): + packet_types.append('Broadcast') + if (packets['unicast']['from'] > 0) or (packets['unicast']['to'] > 0): + packet_types.append('Unicast') + + # Construct the description if any packet types were detected + if packet_types: + description = 'Packet types detected: ' + ', '.join(packet_types) + else: + description = 'No multicast, broadcast or unicast detected' + + except Exception as e: # pylint: disable=W0718 + LOGGER.error(e) + result = 'Error' + return result, description, details + + def get_network_packet_types(self): + combined_results = { + 'mac_address': self._device_mac, + 'multicast': { + 'from': 0, + 'to': 0 + }, + 'broadcast': { + 'from': 0, + 'to': 0 + }, + 'unicast': { + 'from': 0, + 'to': 0 + }, + } + capture_files = [self.startup_capture_file, self.monitor_capture_file] + for capture_file in capture_files: + bin_file = self._bin_dir + '/get_packet_counts.sh' + args = f'"{capture_file}" "{self._device_mac}"' + command = f'{bin_file} {args}' + response = util.run_command(command) + packets = json.loads(response[0].strip()) + # Combine results + combined_results['multicast']['from'] += packets['multicast']['from'] + combined_results['multicast']['to'] += packets['multicast']['to'] + combined_results['broadcast']['from'] += packets['broadcast']['from'] + combined_results['broadcast']['to'] += packets['broadcast']['to'] + combined_results['unicast']['from'] += packets['unicast']['from'] + combined_results['unicast']['to'] += packets['unicast']['to'] + return combined_results + def enable_failover(self): # Move primary DHCP server to primary failover LOGGER.info('Configuring primary failover DHCP server') @@ -500,6 +779,7 @@ def is_ip_in_range(self, ip, start_ip, end_ip): return start_int <= ip_int <= end_int def _run_subnet_test(self, config): + # Resolve the configured dhcp subnet ranges ranges = None if 'ranges' in config: @@ -514,6 +794,7 @@ def _run_subnet_test(self, config): response = self.dhcp1_client.get_dhcp_range() cur_range = {} + if response.code == 200: cur_range['start'] = response.start cur_range['end'] = response.end @@ -526,16 +807,21 @@ def _run_subnet_test(self, config): results = [] dhcp_setup = self.setup_single_dhcp_server() + if dhcp_setup[0]: LOGGER.info(dhcp_setup[1]) lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, timeout=self._lease_wait_time_sec) + if lease is not None: if self._dhcp_util.is_lease_active(lease): results = self.test_subnets(ranges) else: - LOGGER.info('Failed to confirm a valid active lease for the device') - return None, 'Failed to confirm a valid active lease for the device' + LOGGER.info('Device has no current DHCP lease ' + + 'so this test could not be run') + return ( + None, + 'Device has no current DHCP lease so this test could not be run') else: LOGGER.error(dhcp_setup[1]) return None, 'Failed to setup DHCP server for test' @@ -549,7 +835,7 @@ def _run_subnet_test(self, config): else: if result['result'] is not None: final_result &= result['result'] - if result['result']: + if not result['result']: final_result_details += result['details'] + '\n' if final_result: @@ -562,10 +848,17 @@ def _run_subnet_test(self, config): # Wait for the current lease to expire lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, timeout=self._lease_wait_time_sec) - self._dhcp_util.wait_for_lease_expire(lease, self._lease_wait_time_sec) + + # Check if lease is active + if lease is not None: + self._dhcp_util.wait_for_lease_expire(lease, self._lease_wait_time_sec) + else: + # If not, wait for 30 seconds as a fallback + time.sleep(30) # Wait for a new lease to be provided before exiting test # to prevent other test modules from failing + LOGGER.info('Checking for new lease') # Subnet changes tend to take longer to pick up so we'll allow # for twice the lease wait time @@ -580,9 +873,8 @@ def _run_subnet_test(self, config): else: LOGGER.info('New lease not found. Waiting to check again') - except Exception as e: # pylint: disable=W0718 - LOGGER.error('Failed to restore DHCP server configuration: ' + str(e)) - LOGGER.error(traceback.format_exc()) + except Exception: # pylint: disable=W0718 + LOGGER.error('Failed to restore DHCP server configuration') return final_result, final_result_details diff --git a/modules/test/conn/python/src/dhcp_util.py b/modules/test/conn/python/src/dhcp_util.py index be5f0cac2..22880dab0 100644 --- a/modules/test/conn/python/src/dhcp_util.py +++ b/modules/test/conn/python/src/dhcp_util.py @@ -207,7 +207,7 @@ def is_lease_active(self, lease): def ping(self, host): cmd = 'ping -c 1 ' + str(host) - success = util.run_command(cmd, output=False) + success = util.run_command(cmd, output=False) # pylint: disable=E1120 return success def add_reserved_lease(self, @@ -256,21 +256,30 @@ def setup_single_dhcp_server(self): return False def wait_for_lease_expire(self, lease, max_wait_time=30): - expiration_utc = datetime.strptime(lease['expires'], '%Y-%m-%d %H:%M:%S') - # lease information stored in UTC so we need to convert to local time - expiration = self.utc_to_local(expiration_utc) - time_to_expire = expiration - datetime.now(tz=tz.tzlocal()) - # Wait until the expiration time and padd 5 seconds - # If wait time is longer than max_wait_time, only wait - # for the max wait time - wait_time = min(max_wait_time, - time_to_expire.total_seconds() + - 5) if time_to_expire.total_seconds() > 0 else 0 - LOGGER.info('Time until lease expiration: ' + str(wait_time)) - LOGGER.info('Waiting for current lease to expire: ' + str(expiration)) - if wait_time > 0: - time.sleep(wait_time) - LOGGER.info('Current lease expired.') + + try: + expiration_utc = datetime.strptime(lease['expires'], '%Y-%m-%d %H:%M:%S') + + # Lease information stored in UTC so we need to convert to local time + expiration = self.utc_to_local(expiration_utc) + time_to_expire = expiration - datetime.now(tz=tz.tzlocal()) + + # Wait until the expiration time and padd 5 seconds + # If wait time is longer than max_wait_time, only wait + # for the max wait time + wait_time = min(max_wait_time, + time_to_expire.total_seconds() + + 5) if time_to_expire.total_seconds() > 0 else 0 + + LOGGER.info('Time until lease expiration: ' + str(wait_time)) + LOGGER.info('Waiting for current lease to expire: ' + str(expiration)) + + if wait_time > 0: + time.sleep(wait_time) + LOGGER.info('Current lease expired') + + except TypeError: + LOGGER.error('Device does not have an active lease') # Convert from a UTC datetime to the local time zone def utc_to_local(self, utc_datetime): diff --git a/modules/test/conn/python/src/port_stats_util.py b/modules/test/conn/python/src/port_stats_util.py index d923501eb..340cabd02 100644 --- a/modules/test/conn/python/src/port_stats_util.py +++ b/modules/test/conn/python/src/port_stats_util.py @@ -14,12 +14,17 @@ """Module that contains various methods for validating the Port statistics """ import os +import re ETHTOOL_CONN_STATS_FILE = 'runtime/network/ethtool_conn_stats.txt' ETHTOOL_PORT_STATS_PRE_FILE = ( 'runtime/network/ethtool_port_stats_pre_monitor.txt') ETHTOOL_PORT_STATS_POST_FILE = ( 'runtime/network/ethtool_port_stats_post_monitor.txt') +IFCONFIG_PORT_STATS_PRE_FILE = ( + 'runtime/network/ifconfig_port_stats_pre_monitor.txt') +IFCONFIG_PORT_STATS_POST_FILE = ( + 'runtime/network/ifconfig_port_stats_post_monitor.txt') LOG_NAME = 'port_stats_util' LOGGER = None @@ -32,32 +37,33 @@ def __init__(self, logger, ethtool_conn_stats_file=ETHTOOL_CONN_STATS_FILE, ethtool_port_stats_pre_file=ETHTOOL_PORT_STATS_PRE_FILE, - ethtool_port_stats_post_file=ETHTOOL_PORT_STATS_POST_FILE): + ethtool_port_stats_post_file=ETHTOOL_PORT_STATS_POST_FILE, + ifconfig_port_stats_pre_file=IFCONFIG_PORT_STATS_PRE_FILE, + ifconfig_port_stats_post_file=IFCONFIG_PORT_STATS_POST_FILE): self.ethtool_conn_stats_file = ethtool_conn_stats_file self.ethtool_port_stats_pre_file = ethtool_port_stats_pre_file self.ethtool_port_stats_post_file = ethtool_port_stats_post_file + self.ifconfig_port_stats_pre_file = ifconfig_port_stats_pre_file + self.ifconfig_port_stats_post_file = ifconfig_port_stats_post_file global LOGGER LOGGER = logger self.conn_stats = self._read_stats_file(self.ethtool_conn_stats_file) def is_autonegotiate(self): - auto_negotiation = False + auto_negotiation = None auto_negotiation_status = self._get_stat_option(stats=self.conn_stats, option='Auto-negotiation:') if auto_negotiation_status is not None: auto_negotiation = 'on' in auto_negotiation_status return auto_negotiation - def connection_port_link_test(self): + def ethtool_port_link_test(self): stats_pre = self._read_stats_file(self.ethtool_port_stats_pre_file) stats_post = self._read_stats_file(self.ethtool_port_stats_post_file) result = None description = '' details = '' - if stats_pre is None or stats_pre is None: - result = 'Error' - description = 'Port stats not available' - else: + if stats_pre is not None and stats_pre is not None: tx_errors_pre = self._get_stat_option(stats=stats_pre, option='tx_errors:') tx_errors_post = self._get_stat_option(stats=stats_post, @@ -66,24 +72,67 @@ def connection_port_link_test(self): option='rx_errors:') rx_errors_post = self._get_stat_option(stats=stats_post, option='rx_errors:') - tx_errors = int(tx_errors_post) - int(tx_errors_pre) - rx_errors = int(rx_errors_post) - int(rx_errors_pre) - if tx_errors > 0 or rx_errors > 0: - result = False - description = 'Port errors detected' - details = f'TX errors: {tx_errors}, RX errors: {rx_errors}' - else: - result = True - description = 'No port errors detected' + + # Check that the above have been resolved correctly + if (tx_errors_pre is not None and tx_errors_post is not None + and rx_errors_pre is not None and rx_errors_post is not None): + tx_errors = int(tx_errors_post) - int(tx_errors_pre) + rx_errors = int(rx_errors_post) - int(rx_errors_pre) + if tx_errors > 0 or rx_errors > 0: + result = False + description = 'Port errors detected' + details = f'TX errors: {tx_errors}, RX errors: {rx_errors}' + else: + result = True + description = 'No port errors detected' return result, description, details + def ifconfig_port_link_test(self): + stats_pre = self._read_stats_file(self.ifconfig_port_stats_pre_file) + stats_post = self._read_stats_file(self.ifconfig_port_stats_post_file) + result = None + description = '' + details = '' + if stats_pre is not None and stats_pre is not None: + rx_errors_pre, tx_errors_pre = self.extract_rx_tx_error_counts(stats_pre) + rx_errors_post, tx_errors_post = self.extract_rx_tx_error_counts( + stats_post) + + # Check that the above have been resolved correctly + if (tx_errors_pre is not None and tx_errors_post is not None + and rx_errors_pre is not None and rx_errors_post is not None): + tx_errors = int(tx_errors_post) - int(tx_errors_pre) + rx_errors = int(rx_errors_post) - int(rx_errors_pre) + if tx_errors > 0 or rx_errors > 0: + result = False + description = 'Port errors detected' + details = f'TX errors: {tx_errors}, RX errors: {rx_errors}' + else: + result = True + description = 'No port errors detected' + return result, description, details + + def connection_port_link_test(self): + port_results = self.ethtool_port_link_test() + if port_results[0] is None: + port_results = self.ifconfig_port_link_test() + if port_results[0] is None: + result = 'Error' + description = 'Port stats not available' + details = '' + port_results = result, description, details + return port_results + def connection_port_duplex_test(self): auto_negotiation = self.is_autonegotiate() # Calculate final results result = None description = '' details = '' - if not auto_negotiation: + if auto_negotiation is None: + result = 'Error' + description = 'Port stats not available' + elif not auto_negotiation: result = False description = 'Interface not configured for auto-negotiation' else: @@ -104,7 +153,10 @@ def connection_port_speed_test(self): result = None description = '' details = '' - if not auto_negotiation: + if auto_negotiation is None: + result = 'Error' + description = 'Port stats not available' + elif not auto_negotiation: result = False description = 'Interface not configured for auto-negotiation' else: @@ -119,11 +171,19 @@ def connection_port_speed_test(self): details = f'Speed negotiated: {speed}' return result, description, details + def extract_rx_tx_error_counts(self, ifconfig): + rx_match = re.search(r'^\s*RX errors (\d+)', ifconfig, re.MULTILINE) + tx_match = re.search(r'^\s*TX errors (\d+)', ifconfig, re.MULTILINE) + + if rx_match and tx_match: + return int(rx_match.group(1)), int(tx_match.group(1)) + else: + return None, None + def _get_stat_option(self, stats, option): """Extract the requested parameter from the ethtool result""" value = None for line in stats.split('\n'): - #LOGGER.info(f'Checking option: {line}') if line.startswith(f'{option}'): value = line.split(':')[1].strip() break diff --git a/modules/test/dns/README.md b/modules/test/dns/README.md index 13f0df5fd..c2a917b13 100644 --- a/modules/test/dns/README.md +++ b/modules/test/dns/README.md @@ -15,4 +15,5 @@ Within the ```python/src``` directory, the below tests are executed. | ID | Description | Expected behavior | Required result |---|---|---|---| | dns.network.hostname_resolution | Verifies that the device resolves hostnames | The device sends DNS requests | Required | -| dns.network.from_dhcp | Verifies that the device allows for a DNS server to be provided by the DHCP server | The device sends DNS requests to the DNS server provided by the DHCP server | Roadmap | \ No newline at end of file +| dns.network.from_dhcp | Verifies that the device allows for a DNS server to be provided by the DHCP server | The device sends DNS requests to the DNS server provided by the DHCP server | Informational | +| dns.mdns | Does the device has MDNS (or any kind of IP multicast) | Device may send MDNS requests | Informational | \ No newline at end of file diff --git a/modules/test/dns/bin/start_test_module b/modules/test/dns/bin/start_test_module index a529c2fcf..c3209261a 100644 --- a/modules/test/dns/bin/start_test_module +++ b/modules/test/dns/bin/start_test_module @@ -41,11 +41,8 @@ else fi # Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python scrip that will execute the tests for this module diff --git a/modules/test/dns/conf/module_config.json b/modules/test/dns/conf/module_config.json index 13c9b3236..662273cd7 100644 --- a/modules/test/dns/conf/module_config.json +++ b/modules/test/dns/conf/module_config.json @@ -16,7 +16,6 @@ "name": "dns.network.hostname_resolution", "test_description": "Verify the device sends DNS requests", "expected_behavior": "The device sends DNS requests.", - "required_result": "Required", "recommendations": [ "Install a supported DNS client", "Ensure DNS servers have been set correctly", @@ -27,10 +26,14 @@ "name": "dns.network.from_dhcp", "test_description": "Verify the device allows for a DNS server to be entered automatically", "expected_behavior": "The device sends DNS requests to the DNS server provided by the DHCP server", - "required_result": "Informational", "recommendations": [ "Install a DNS client that supports fetching DNS servers from DHCP options" ] + }, + { + "name": "dns.mdns", + "test_description": "Does the device have MDNS (or any kind of IP multicast)", + "expected_behavior": "Device may send MDNS requests" } ] } diff --git a/modules/test/dns/dns.Dockerfile b/modules/test/dns/dns.Dockerfile index 0197fd72e..53f8f31f8 100644 --- a/modules/test/dns/dns.Dockerfile +++ b/modules/test/dns/dns.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/conn-test -FROM test-run/base-test:latest +# Image name: testrun/conn-test +FROM testrun/base-test:latest ARG MODULE_NAME=dns ARG MODULE_DIR=modules/test/$MODULE_NAME @@ -22,7 +22,7 @@ ARG MODULE_DIR=modules/test/$MODULE_NAME COPY $MODULE_DIR/python/requirements.txt /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf @@ -31,4 +31,8 @@ COPY $MODULE_DIR/conf /testrun/conf COPY $MODULE_DIR/bin /testrun/bin # Copy over all python files -COPY $MODULE_DIR/python /testrun/python \ No newline at end of file +COPY $MODULE_DIR/python /testrun/python + +# Copy Jinja template +COPY $MODULE_DIR/resources/report_template.jinja2 $REPORT_TEMPLATE_PATH/ + diff --git a/modules/test/dns/python/requirements.txt b/modules/test/dns/python/requirements.txt index 93b351f44..f61132516 100644 --- a/modules/test/dns/python/requirements.txt +++ b/modules/test/dns/python/requirements.txt @@ -1 +1,6 @@ -scapy \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies + +# User defined packages +scapy==2.6.0 diff --git a/modules/test/dns/python/src/dns_module.py b/modules/test/dns/python/src/dns_module.py index 607a026b5..fc735d002 100644 --- a/modules/test/dns/python/src/dns_module.py +++ b/modules/test/dns/python/src/dns_module.py @@ -13,24 +13,27 @@ # limitations under the License. """DNS test module""" import subprocess -from scapy.all import rdpcap, DNS, IP +from scapy.all import rdpcap, DNS, IP, Ether, DNSRR +from scapy.error import Scapy_Exception from test_module import TestModule import os +from collections import Counter +from jinja2 import Environment, FileSystemLoader LOG_NAME = 'test_dns' -MODULE_REPORT_FILE_NAME='dns_report.html' +MODULE_REPORT_FILE_NAME = 'dns_report.j2.html' DNS_SERVER_CAPTURE_FILE = '/runtime/network/dns.pcap' STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap' MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap' LOGGER = None +REPORT_TEMPLATE_FILE = 'report_template.jinja2' class DNSModule(TestModule): """DNS Test module""" - def __init__(self, + def __init__(self, # pylint: disable=R0917 module, - log_dir=None, conf_file=None, results_dir=None, dns_server_capture_file=DNS_SERVER_CAPTURE_FILE, @@ -38,102 +41,114 @@ def __init__(self, monitor_capture_file=MONITOR_CAPTURE_FILE): super().__init__(module_name=module, log_name=LOG_NAME, - log_dir=log_dir, conf_file=conf_file, results_dir=results_dir) - self.dns_server_capture_file=dns_server_capture_file - self.startup_capture_file=startup_capture_file - self.monitor_capture_file=monitor_capture_file + self.dns_server_capture_file = dns_server_capture_file + self.startup_capture_file = startup_capture_file + self.monitor_capture_file = monitor_capture_file self._dns_server = '10.10.10.4' global LOGGER LOGGER = self._get_logger() def generate_module_report(self): + # Load Jinja2 template + page_max_height = 850 + header_height = 48 + summary_height = 135 + row_height = 44 + loader=FileSystemLoader(self._report_template_folder) + template = Environment( + loader=loader, + trim_blocks=True, + lstrip_blocks=True + ).get_template(REPORT_TEMPLATE_FILE) + module_header='DNS Module' + # Summary table headers + summary_headers = [ + 'Requests to local DNS server', + 'Requests to external DNS servers', + 'Total DNS requests', + 'Total DNS responses', + ] + # Module data Headers + module_data_headers = [ + 'Source', + 'Destination', + 'Resolved IP', + 'Type', + 'URL', + 'Count', + ] # Extract DNS data from the pcap file dns_table_data = self.extract_dns_data() - html_content = '

DNS Module

' - # Set the summary variables - local_requests = sum(1 for row in dns_table_data - if row['Destination'] == - self._dns_server and row['Type'] == 'Query') - external_requests = sum(1 for row in dns_table_data - if row['Destination'] != - self._dns_server and row['Type'] == 'Query') + local_requests = sum( + 1 for row in dns_table_data + if row['Destination'] == self._dns_server and row['Type'] == 'Query') + external_requests = sum( + 1 for row in dns_table_data + if row['Destination'] != self._dns_server and row['Type'] == 'Query') - total_requests = sum(1 for row in dns_table_data - if row['Type'] == 'Query') + total_requests = sum(1 for row in dns_table_data if row['Type'] == 'Query') total_responses = sum(1 for row in dns_table_data - if row['Type'] == 'Response') + if row['Type'] == 'Response') # Add summary table - html_content += (f''' -
- - - - - - - - - - - - - - - -
Requests to local DNS serverRequests to external DNS serversTotal DNS requestsTotal DNS responses
{local_requests}{external_requests}{total_requests}{total_responses}
- ''') - + summary_data = [ + local_requests, + external_requests, + total_requests, + total_responses, + ] + + module_data = [] if (total_requests + total_responses) > 0: - table_content = ''' - - - - - - - - - - ''' - - for row in dns_table_data: - table_content += (f''' - - - - - - ''') - - table_content += ''' - -
SourceDestinationTypeURL
{row['Source']}{row['Destination']}{row['Type']}{row['Data']}
- ''' - - html_content += table_content - - else: - html_content += (''' -
-
- No DNS traffic detected from the device -
''') - - LOGGER.debug('Module report:\n' + html_content) + # Count unique combinations + counter = Counter((row['Source'], row['Destination'], row['ResolvedIP'], + row['Type'], row['Data']) for row in dns_table_data) + + # Generate the HTML table with the count column + for (src, dst, res_ip, typ, dat), count in counter.items(): + module_data.append({ + 'src': src, + 'dst': dst, + 'res_ip': res_ip, + 'typ': typ, + 'dat': dat, + 'count': count, + }) + # Handling the possible table split + table_height = (len(module_data) + 1) * row_height + page_useful_space = page_max_height - header_height - summary_height + pages = table_height // (page_useful_space) + rows_on_page = (page_useful_space) // row_height + start = 0 + report_html = '' + for page in range(pages+1): + end = start + min(len(module_data), rows_on_page) + module_header_repr = module_header if page == 0 else None + page_html = template.render( + base_template=self._base_template_file, + module_header=module_header_repr, + summary_headers=summary_headers, + summary_data=summary_data, + module_data_headers=module_data_headers, + module_data=module_data[start:end] + ) + report_html += page_html + start = end + + LOGGER.debug('Module report:\n' + report_html) # Use os.path.join to create the complete file path report_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME) # Write the content to a file with open(report_path, 'w', encoding='utf-8') as file: - file.write(html_content) + file.write(report_html) LOGGER.info('Module report generated at: ' + str(report_path)) @@ -142,37 +157,62 @@ def generate_module_report(self): def extract_dns_data(self): dns_data = [] - # Read the pcap file - packets = rdpcap(self.dns_server_capture_file) + rdpcap( - self.startup_capture_file) + rdpcap(self.monitor_capture_file) + # Read the startup and monitor pcap files + packets = (rdpcap(self.startup_capture_file) + + rdpcap(self.monitor_capture_file)) + + # Read the dns.pcap file + try: + packets += rdpcap(self.dns_server_capture_file) + except (FileNotFoundError, Scapy_Exception): + LOGGER.error('dns.pcap not found or empty, ignoring') # Iterate through DNS packets for packet in packets: if DNS in packet and packet.haslayer(IP): - source_ip = packet[IP].src - destination_ip = packet[IP].dst - dns_layer = packet[DNS] - - # 'qr' field indicates query (0) or response (1) - dns_type = 'Query' if dns_layer.qr == 0 else 'Response' - - # Check for the presence of DNS query name - if hasattr(dns_layer, 'qd') and dns_layer.qd is not None: + + # Check if either source or destination MAC matches the device + if self._device_mac in [packet[Ether].src, packet[Ether].dst]: + source_ip = packet[IP].src + destination_ip = packet[IP].dst + dns_layer = packet[DNS] + # 'qr' field indicates query (0) or response (1) + dns_type = 'Query' if dns_layer.qr == 0 else 'Response' + + # Check if 'qd' (query data) exists and has at least one entry + if hasattr(dns_layer, 'qd') and dns_layer.qdcount > 0: qname = dns_layer.qd.qname.decode() if dns_layer.qd.qname else 'N/A' - else: + else: qname = 'N/A' - dns_data.append({ - 'Timestamp': float(packet.time), # Timestamp of the DNS packet - 'Source': source_ip, - 'Destination': destination_ip, - 'Type': dns_type, - 'Data': qname[:-1] - }) + resolved_ip = 'N/A' + # If it's a response packet, extract the resolved IP address + # from the answer section + if dns_layer.qr == 1 and hasattr(dns_layer, + 'an') and dns_layer.ancount > 0: + # Loop through all answers in the DNS response + for i in range(min(dns_layer.ancount, len(dns_layer.an))): + answer = dns_layer.an[i] + # Check if the answer is of type DNSRR + if isinstance(answer, DNSRR): + # Check for IPv4 (A record) or IPv6 (AAAA record) + if answer.type == 1: # Indicates an A record (IPv4 address) + resolved_ip = answer.rdata # Extract IPv4 address + break # Stop after finding the first valid resolved IP + elif answer.type == 28: # Indicates AAAA record (IPv6 address) + resolved_ip = answer.rdata # Extract IPv6 address + break # Stop after finding the first valid resolved IP + + dns_data.append({ + 'Timestamp': float(packet.time), # Timestamp of the DNS packet + 'Source': source_ip, + 'Destination': destination_ip, + 'ResolvedIP': resolved_ip, # Adding the resolved IP address + 'Type': dns_type, + 'Data': qname[:-1] + }) # Filter unique entries based on 'Timestamp' - # DNS Server will duplicate messages caught by - # startup and monitor filtered_unique_dns_data = [] seen_timestamps = set() @@ -186,15 +226,15 @@ def extract_dns_data(self): def _has_dns_traffic(self, tcpdump_filter): dns_server_queries = self._exec_tcpdump(tcpdump_filter, - DNS_SERVER_CAPTURE_FILE) + self.dns_server_capture_file) LOGGER.info('DNS Server queries found: ' + str(len(dns_server_queries))) dns_startup_queries = self._exec_tcpdump(tcpdump_filter, - STARTUP_CAPTURE_FILE) + self.startup_capture_file) LOGGER.info('Startup DNS queries found: ' + str(len(dns_startup_queries))) dns_monitor_queries = self._exec_tcpdump(tcpdump_filter, - MONITOR_CAPTURE_FILE) + self.monitor_capture_file) LOGGER.info('Monitor DNS queries found: ' + str(len(dns_monitor_queries))) num_query_dns = len(dns_server_queries) + len(dns_startup_queries) + len( @@ -203,6 +243,10 @@ def _has_dns_traffic(self, tcpdump_filter): return num_query_dns > 0 + # Added to access the method for dns unittests + def dns_network_from_dhcp(self): + return self._dns_network_from_dhcp() + def _dns_network_from_dhcp(self): LOGGER.info('Running dns.network.from_dhcp') LOGGER.info('Checking DNS traffic for configured DHCP DNS server: ' + @@ -215,10 +259,9 @@ def _dns_network_from_dhcp(self): dns_packets_local = self._has_dns_traffic(tcpdump_filter=tcpdump_filter) # Check if the device sends any DNS traffic to non-DHCP provided server - tcpdump_filter = (f'dst port 53 and dst not host {self._dns_server} ' + - 'ether src {self._device_mac}') + tcpdump_filter = (f'dst port 53 and not dst host {self._dns_server} ' + + f'and ether src {self._device_mac}') dns_packets_not_local = self._has_dns_traffic(tcpdump_filter=tcpdump_filter) - if dns_packets_local or dns_packets_not_local: if dns_packets_not_local: description = 'DNS traffic detected to non-DHCP provided server' @@ -226,8 +269,10 @@ def _dns_network_from_dhcp(self): LOGGER.info('DNS traffic detected only to configured DHCP DNS server') description = 'DNS traffic detected only to DHCP provided server' else: - LOGGER.info('No DNS traffic detected from the device') - description = 'No DNS traffic detected from the device' + LOGGER.info( + 'No DNS traffic detected from the device to the DHCP DNS server') + description = '' \ + 'No DNS traffic detected from the device to the DHCP DNS server' return 'Informational', description def _dns_network_hostname_resolution(self): @@ -273,10 +318,10 @@ def _exec_tcpdump(self, tcpdump_filter, capture_file): LOGGER.debug('tcpdump command: ' + command) with subprocess.Popen(command, - universal_newlines=True, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as process: + universal_newlines=True, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) as process: text = str(process.stdout.read()).rstrip() LOGGER.debug('tcpdump response: ' + text) diff --git a/modules/test/dns/resources/report_template.jinja2 b/modules/test/dns/resources/report_template.jinja2 new file mode 100644 index 000000000..8e701a8e3 --- /dev/null +++ b/modules/test/dns/resources/report_template.jinja2 @@ -0,0 +1,31 @@ +{% extends base_template %} +{% block content %} +{% if module_data %} + + + + {% for header in module_data_headers %} + + {% endfor %} + + + + {% for row in module_data %} + + + + + + + + + {% endfor %} + +
{{ header }}
{{ row['src'] }}
{{ row['dst'] }}
{{ row['res_ip'] }}
{{ row['typ'] }}
{{ row['dat'] }}
{{ row['count'] }}
+{% else %} +
+
+ No DNS traffic detected from the device +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/modules/test/ntp/bin/start_test_module b/modules/test/ntp/bin/start_test_module index a09349cf9..33b2881f4 100644 --- a/modules/test/ntp/bin/start_test_module +++ b/modules/test/ntp/bin/start_test_module @@ -27,11 +27,8 @@ else fi # Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python scrip that will execute the tests for this module diff --git a/modules/test/ntp/conf/module_config.json b/modules/test/ntp/conf/module_config.json index 55eb3df76..6634b127d 100644 --- a/modules/test/ntp/conf/module_config.json +++ b/modules/test/ntp/conf/module_config.json @@ -9,14 +9,13 @@ "docker": { "depends_on": "base", "enable_container": true, - "timeout": 30 + "timeout": 60 }, "tests":[ { "name": "ntp.network.ntp_support", "test_description": "Does the device request network time sync as client as per RFC 5905 - Network Time Protocol Version 4: Protocol and Algorithms Specification", "expected_behavior": "The device sends an NTPv4 request to the configured NTP server.", - "required_result": "Required", "recommendations": [ "Set the NTP version to v4 in the NTP client", "Install an NTP client that supports NTPv4" @@ -26,7 +25,6 @@ "name": "ntp.network.ntp_dhcp", "test_description": "Accept NTP address over DHCP", "expected_behavior": "Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)", - "required_result": "Roadmap", "recommendations": [ "Install an NTP client that supports fetching the NTP servers from DHCP options" ] diff --git a/modules/test/ntp/ntp.Dockerfile b/modules/test/ntp/ntp.Dockerfile index 33b06287e..c7ae7fee1 100644 --- a/modules/test/ntp/ntp.Dockerfile +++ b/modules/test/ntp/ntp.Dockerfile @@ -1,5 +1,19 @@ -# Image name: test-run/ntp-test -FROM test-run/base-test:latest +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Image name: testrun/ntp-test +FROM testrun/base-test:latest ARG MODULE_NAME=ntp ARG MODULE_DIR=modules/test/$MODULE_NAME @@ -8,7 +22,7 @@ ARG MODULE_DIR=modules/test/$MODULE_NAME COPY $MODULE_DIR/python/requirements.txt /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf @@ -17,4 +31,7 @@ COPY $MODULE_DIR/conf /testrun/conf COPY $MODULE_DIR/bin /testrun/bin # Copy over all python files -COPY $MODULE_DIR/python /testrun/python \ No newline at end of file +COPY $MODULE_DIR/python /testrun/python + +# Copy Jinja template +COPY $MODULE_DIR/resources/report_template.jinja2 $REPORT_TEMPLATE_PATH/ \ No newline at end of file diff --git a/modules/test/ntp/python/requirements.txt b/modules/test/ntp/python/requirements.txt index 93b351f44..f61132516 100644 --- a/modules/test/ntp/python/requirements.txt +++ b/modules/test/ntp/python/requirements.txt @@ -1 +1,6 @@ -scapy \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies + +# User defined packages +scapy==2.6.0 diff --git a/modules/test/ntp/python/src/ntp_module.py b/modules/test/ntp/python/src/ntp_module.py index 453c992e6..8bc609502 100644 --- a/modules/test/ntp/python/src/ntp_module.py +++ b/modules/test/ntp/python/src/ntp_module.py @@ -14,23 +14,25 @@ """NTP test module""" from test_module import TestModule from scapy.all import rdpcap, IP, IPv6, NTP, UDP, Ether -from datetime import datetime +from scapy.error import Scapy_Exception import os +from collections import defaultdict +from jinja2 import Environment, FileSystemLoader LOG_NAME = 'test_ntp' -MODULE_REPORT_FILE_NAME = 'ntp_report.html' +MODULE_REPORT_FILE_NAME = 'ntp_report.j2.html' NTP_SERVER_CAPTURE_FILE = '/runtime/network/ntp.pcap' STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap' MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap' LOGGER = None +REPORT_TEMPLATE_FILE = 'report_template.jinja2' class NTPModule(TestModule): """NTP Test module""" - def __init__(self, + def __init__(self, # pylint: disable=R0917 module, - log_dir=None, conf_file=None, results_dir=None, ntp_server_capture_file=NTP_SERVER_CAPTURE_FILE, @@ -38,7 +40,6 @@ def __init__(self, monitor_capture_file=MONITOR_CAPTURE_FILE): super().__init__(module_name=module, log_name=LOG_NAME, - log_dir=log_dir, conf_file=conf_file, results_dir=results_dir) self.ntp_server_capture_file = ntp_server_capture_file @@ -51,11 +52,38 @@ def __init__(self, LOGGER = self._get_logger() def generate_module_report(self): + # Load Jinja2 template + page_max_height = 910 + header_height = 48 + summary_height = 135 + row_height = 42 + loader=FileSystemLoader(self._report_template_folder) + template = Environment( + loader=loader, + trim_blocks=True, + lstrip_blocks=True, + ).get_template(REPORT_TEMPLATE_FILE) + module_header='NTP Module' + # Summary table headers + summary_headers = [ + 'Requests to local NTP server', + 'Requests to external NTP servers', + 'Total NTP requests', + 'Total NTP responses' + ] + # Module data Headers + module_data_headers = [ + 'Source', + 'Destination', + 'Type', + 'Version', + 'Count', + 'Sync Request Average', + ] + # Extract NTP data from the pcap file ntp_table_data = self.extract_ntp_data() - html_content = '

NTP Module

' - # Set the summary variables local_requests = sum( 1 for row in ntp_table_data @@ -69,86 +97,96 @@ def generate_module_report(self): total_responses = sum(1 for row in ntp_table_data if row['Type'] == 'Server') - # Add summary table - html_content += (f''' - - - - - - - - - - - - - - - - - -
Requests to local NTP serverRequests to external NTP serversTotal NTP requestsTotal NTP responses
{local_requests}{external_requests}{total_requests}{total_responses}
- ''') + # Summary table data + summary_data = [ + local_requests, + external_requests, + total_requests, + total_responses + ] + + # Initialize a dictionary to store timestamps for each unique combination + timestamps = defaultdict(list) + + # Collect timestamps for each unique combination + for row in ntp_table_data: + # Add the timestamp to the corresponding combination + key = (row['Source'], row['Destination'], row['Type'], row['Version']) + timestamps[key].append(row['Timestamp']) + + # Calculate the average time between requests for each unique combination + average_time_between_requests = {} + + for key, times in timestamps.items(): + # Sort the timestamps + times.sort() + + # Calculate the time differences between consecutive timestamps + time_diffs = [t2 - t1 for t1, t2 in zip(times[:-1], times[1:])] + # Calculate the average of the time differences + if time_diffs: + avg_diff = sum(time_diffs) / len(time_diffs) + else: + avg_diff = 0 # one timestamp, the average difference is 0 + + average_time_between_requests[key] = avg_diff + + # Module table data + module_table_data = [] if total_requests + total_responses > 0: - table_content = ''' - - - - - - - - - - - ''' - - for row in ntp_table_data: - - # Timestamp of the NTP packet - dt_object = datetime.utcfromtimestamp(row['Timestamp']) - - # Extract milliseconds from the fractional part of the timestamp - milliseconds = int((row['Timestamp'] % 1) * 1000) - - # Format the datetime object with milliseconds - formatted_time = dt_object.strftime( - '%b %d, %Y %H:%M:%S.') + f'{milliseconds:03d}' - - table_content += (f''' - - - - - - - ''') - - table_content += ''' - -
SourceDestinationTypeVersionTimestamp
{row['Source']}{row['Destination']}{row['Type']}{row['Version']}{formatted_time}
- ''' - - html_content += table_content - - else: - html_content += (''' -
-
- No NTP traffic detected from the device -
''') - - LOGGER.debug('Module report:\n' + html_content) + # Generate the HTML table with the count column + for (src, dst, typ, + version), avg_diff in average_time_between_requests.items(): + cnt = len(timestamps[(src, dst, typ, version)]) + + # Sync Average only applies to client requests + if 'Client' in typ: + # Convert avg_diff to seconds and format it + avg_diff_seconds = avg_diff + avg_formatted_time = f'{avg_diff_seconds:.3f} seconds' + else: + avg_formatted_time = 'N/A' + + module_table_data.append({ + 'src': src, + 'dst': dst, + 'typ': typ, + 'version': version, + 'cnt': cnt, + 'avg_fmt': avg_formatted_time + }) + + # Handling the possible table split + table_height = (len(module_table_data) + 1) * row_height + page_useful_space = page_max_height - header_height - summary_height + pages = table_height // (page_useful_space) + rows_on_page = ((page_useful_space) // row_height) - 1 + start = 0 + report_html = '' + for page in range(pages+1): + end = start + min(len(module_table_data), rows_on_page) + module_header_repr = module_header if page == 0 else None + page_html = template.render( + base_template=self._base_template_file, + module_header=module_header_repr, + summary_headers=summary_headers, + summary_data=summary_data, + module_data_headers=module_data_headers, + module_data=module_table_data[start:end] + ) + report_html += page_html + start = end + + LOGGER.debug('Module report:\n' + report_html) # Use os.path.join to create the complete file path report_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME) # Write the content to a file with open(report_path, 'w', encoding='utf-8') as file: - file.write(html_content) + file.write(report_html) LOGGER.info('Module report generated at: ' + str(report_path)) @@ -159,8 +197,12 @@ def extract_ntp_data(self): # Read the pcap files packets = (rdpcap(self.startup_capture_file) + - rdpcap(self.monitor_capture_file) + - rdpcap(self.ntp_server_capture_file)) + rdpcap(self.monitor_capture_file)) + + try: + packets += rdpcap(self.ntp_server_capture_file) + except (FileNotFoundError, Scapy_Exception): + LOGGER.error('ntp.pcap not found or empty, ignoring') # Iterate through NTP packets for packet in packets: @@ -171,6 +213,10 @@ def extract_ntp_data(self): # Local NTP server syncs to external servers so we need to filter only # for traffic to/from the device if self._device_mac in (source_mac, destination_mac): + + source_ip = None + dest_ip = None + if IP in packet: source_ip = packet[IP].src dest_ip = packet[IP].dst @@ -208,9 +254,15 @@ def extract_ntp_data(self): def _ntp_network_ntp_support(self): LOGGER.info('Running ntp.network.ntp_support') - packet_capture = (rdpcap(STARTUP_CAPTURE_FILE) + - rdpcap(MONITOR_CAPTURE_FILE) + - rdpcap(NTP_SERVER_CAPTURE_FILE)) + + # Read the pcap files + packet_capture = (rdpcap(self.startup_capture_file) + + rdpcap(self.monitor_capture_file)) + + try: + packet_capture += rdpcap(self.ntp_server_capture_file) + except (FileNotFoundError, Scapy_Exception): + LOGGER.error('ntp.pcap not found or empty, ignoring') device_sends_ntp4 = False device_sends_ntp3 = False @@ -218,6 +270,9 @@ def _ntp_network_ntp_support(self): for packet in packet_capture: if NTP in packet and packet.src == self._device_mac: + + dest_ip = None + if IP in packet: dest_ip = packet[IP].dst elif IPv6 in packet: @@ -229,24 +284,29 @@ def _ntp_network_ntp_support(self): device_sends_ntp3 = True LOGGER.info(f'Device sent NTPv3 request to {dest_ip}') - if not (device_sends_ntp3 or device_sends_ntp4): - result = False, 'Device has not sent any NTP requests' - elif device_sends_ntp3 and device_sends_ntp4: - result = False, ('Device sent NTPv3 and NTPv4 packets. ' + - 'NTPv3 is not allowed.') + result = False, 'Device has not sent any NTP requests' + + if device_sends_ntp3 and device_sends_ntp4: + result = True, ('Device sent NTPv3 and NTPv4 packets') elif device_sends_ntp3: - result = False, ('Device sent NTPv3 packets. ' - 'NTPv3 is not allowed.') + result = False, ('Device sent NTPv3 packets') elif device_sends_ntp4: - result = True, 'Device sent NTPv4 packets.' + result = True, 'Device sent NTPv4 packets' + LOGGER.info(result[1]) return result def _ntp_network_ntp_dhcp(self): LOGGER.info('Running ntp.network.ntp_dhcp') - packet_capture = (rdpcap(STARTUP_CAPTURE_FILE) + - rdpcap(MONITOR_CAPTURE_FILE) + - rdpcap(NTP_SERVER_CAPTURE_FILE)) + + # Read the pcap files + packet_capture = (rdpcap(self.startup_capture_file) + + rdpcap(self.monitor_capture_file)) + + try: + packet_capture += rdpcap(self.ntp_server_capture_file) + except (FileNotFoundError, Scapy_Exception): + LOGGER.error('ntp.pcap not found or empty, ignoring') device_sends_ntp = False ntp_to_local = False @@ -255,6 +315,7 @@ def _ntp_network_ntp_dhcp(self): for packet in packet_capture: if NTP in packet and packet.src == self._device_mac: device_sends_ntp = True + dest_ip = None if IP in packet: dest_ip = packet[IP].dst elif IPv6 in packet: @@ -266,17 +327,17 @@ def _ntp_network_ntp_dhcp(self): LOGGER.info('Device sent NTP request to non-DHCP provided NTP server') ntp_to_remote = True + result = 'Feature Not Detected', 'Device has not sent any NTP requests' + if device_sends_ntp: if ntp_to_local and ntp_to_remote: result = False, ('Device sent NTP request to DHCP provided ' + 'server and non-DHCP provided server') elif ntp_to_remote: result = ('Feature Not Detected', - 'Device sent NTP request to non-DHCP provided server') + 'Device sent NTP request to non-DHCP provided server') elif ntp_to_local: result = True, 'Device sent NTP request to DHCP provided server' - else: - result = 'Feature Not Detected', 'Device has not sent any NTP requests' LOGGER.info(result[1]) return result diff --git a/modules/test/ntp/resources/report_template.jinja2 b/modules/test/ntp/resources/report_template.jinja2 new file mode 100644 index 000000000..c3601b67b --- /dev/null +++ b/modules/test/ntp/resources/report_template.jinja2 @@ -0,0 +1,31 @@ +{% extends base_template %} +{% block content %} +{% if module_data %} + + + + {% for header in module_data_headers %} + + {% endfor %} + + + + {% for row in module_data %} + + + + + + + + + {% endfor %} + +
{{ header }}
{{ row['src'] }}{{ row['dst'] }}{{ row['typ'] }}{{ row['version'] }}{{ row['cnt'] }}{{ row['avg_fmt'] }}
+{% else %} +
+
+ No NTP traffic detected from the device +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/modules/test/protocol/README.md b/modules/test/protocol/README.md index 765fbf758..08c8cd345 100644 --- a/modules/test/protocol/README.md +++ b/modules/test/protocol/README.md @@ -14,6 +14,6 @@ Within the ```python/src``` directory, the below tests are executed. | ID | Description | Expected behavior | Required result |---|---|---|---| -| protocol.valid_bacnet | Can valid BACnet traffic be seen | BACnet traffic can be seen on the network and packets are valid | Required if Applicable | +| protocol.valid_bacnet | Can valid BACnet traffic be seen | BACnet traffic can be seen on the network and packets are valid | Recommended | | protocol.bacnet.version | Obtain the version of BACnet client used | The BACnet client implements an up to date version of BACnet | Recommended | | protocol.valid_modbus | Can valid Modbus traffic be seen | Any Modbus functionality works as expected and valid Modbus traffic can be observed | Recommended | \ No newline at end of file diff --git a/modules/test/protocol/bin/get_bacnet_packets.sh b/modules/test/protocol/bin/get_bacnet_packets.sh old mode 100644 new mode 100755 diff --git a/modules/test/protocol/bin/start_test_module b/modules/test/protocol/bin/start_test_module index a0754836c..e51fdb7ed 100644 --- a/modules/test/protocol/bin/start_test_module +++ b/modules/test/protocol/bin/start_test_module @@ -1,53 +1,50 @@ -#!/bin/bash - -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Setup and start the connection test module - -# Define where the python source files are located -PYTHON_SRC_DIR=/testrun/python/src - -# Fetch module name -MODULE_NAME=$1 - -# Default interface should be veth0 for all containers -DEFAULT_IFACE=veth0 - -# Allow a user to define an interface by passing it into this script -DEFINED_IFACE=$2 - -# Select which interace to use -if [[ -z $DEFINED_IFACE || "$DEFINED_IFACE" == "null" ]] -then - echo "No interface defined, defaulting to veth0" - INTF=$DEFAULT_IFACE -else - INTF=$DEFINED_IFACE -fi - -# Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log -RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE -touch $RESULT_FILE -chown $HOST_USER $LOG_FILE -chown $HOST_USER $RESULT_FILE - -# Run the python script that will execute the tests for this module -# -u flag allows python print statements -# to be logged by docker by running unbuffered -python3 -u $PYTHON_SRC_DIR/run.py "-m $MODULE_NAME" - +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Setup and start the connection test module + +# Define where the python source files are located +PYTHON_SRC_DIR=/testrun/python/src + +# Fetch module name +MODULE_NAME=$1 + +# Default interface should be veth0 for all containers +DEFAULT_IFACE=veth0 + +# Allow a user to define an interface by passing it into this script +DEFINED_IFACE=$2 + +# Select which interace to use +if [[ -z $DEFINED_IFACE || "$DEFINED_IFACE" == "null" ]] +then + echo "No interface defined, defaulting to veth0" + INTF=$DEFAULT_IFACE +else + INTF=$DEFINED_IFACE +fi + +# Create and set permissions on the log files +RESULT_FILE=/runtime/output/$MODULE_NAME-result.json +touch $RESULT_FILE +chown $HOST_USER $RESULT_FILE + +# Run the python script that will execute the tests for this module +# -u flag allows python print statements +# to be logged by docker by running unbuffered +python3 -u $PYTHON_SRC_DIR/run.py "-m $MODULE_NAME" + echo Module has finished \ No newline at end of file diff --git a/modules/test/protocol/conf/module_config.json b/modules/test/protocol/conf/module_config.json index 365bd346b..554f43cc7 100644 --- a/modules/test/protocol/conf/module_config.json +++ b/modules/test/protocol/conf/module_config.json @@ -16,20 +16,17 @@ { "name": "protocol.valid_bacnet", "test_description": "Can valid BACnet traffic be seen", - "expected_behavior": "BACnet traffic can be seen on the network and packets are valid and not malformed", - "required_result": "Recommended" + "expected_behavior": "BACnet traffic can be seen on the network and packets are valid and not malformed" }, { "name": "protocol.bacnet.version", "test_description": "Obtain the version of BACnet client used", - "expected_behavior": "The BACnet client implements an up to date version of BACnet", - "required_result": "Recommended" + "expected_behavior": "The BACnet client implements an up to date version of BACnet" }, { "name": "protocol.valid_modbus", "test_description": "Can valid Modbus traffic be seen", "expected_behavior": "Any Modbus functionality works as expected and valid Modbus traffic can be observed", - "required_result": "Recommended", "config":{ "port": 502, "device_id": 1, diff --git a/modules/test/protocol/protocol.Dockerfile b/modules/test/protocol/protocol.Dockerfile index 6f55520e1..4494ae94e 100644 --- a/modules/test/protocol/protocol.Dockerfile +++ b/modules/test/protocol/protocol.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/protocol-test -FROM test-run/base-test:latest +# Image name: testrun/protocol-test +FROM testrun/base-test:latest # Set DEBIAN_FRONTEND to noninteractive mode ENV DEBIAN_FRONTEND=noninteractive @@ -28,7 +28,7 @@ ARG MODULE_DIR=modules/test/$MODULE_NAME COPY $MODULE_DIR/python/requirements.txt /testrun/python #Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf diff --git a/modules/test/protocol/python/requirements.txt b/modules/test/protocol/python/requirements.txt index 57917735d..1fe889fe9 100644 --- a/modules/test/protocol/python/requirements.txt +++ b/modules/test/protocol/python/requirements.txt @@ -1,7 +1,14 @@ -# Required for BACnet protocol tests -netifaces -BAC0 -pytz - -# Required for Modbus protocol tests -pymodbus \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +bacpypes==0.18.7 +colorama==0.4.6 + +# User defined packages +# Required for BACnet protocol tests +netifaces==0.11.0 +BAC0==23.7.3 +pytz==2024.2 + +# Required for Modbus protocol tests +pymodbus==3.7.4 diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py index a17c9cdd3..3b6d8f0ce 100644 --- a/modules/test/protocol/python/src/protocol_bacnet.py +++ b/modules/test/protocol/python/src/protocol_bacnet.py @@ -29,7 +29,6 @@ DEFAULT_CAPTURE_FILE = 'protocol.pcap' DEFAULT_BIN_DIR = '/testrun/bin' - class BACnet(): """BACnet Test module""" @@ -82,8 +81,10 @@ def validate_device(self): for device in self.devices: object_id = str(device[3]) # BACnet Object ID LOGGER.info('Checking device: ' + str(device)) - result &= self.validate_bacnet_source( + device_valid = self.validate_bacnet_source( object_id=object_id, device_hw_addr=self.device_hw_addr) + if device_valid is not None: + result &= device_valid description = ('BACnet device discovered' if result else 'BACnet device was found but was not device under test') else: @@ -91,17 +92,17 @@ def validate_device(self): description = 'BACnet device could not be discovered' LOGGER.info(description) except Exception: # pylint: disable=W0718 - LOGGER.error('Error occured when validating device', exc_info=True) + LOGGER.error('Error occurred when validating device', exc_info=True) return result, description - def validate_protocol_version(self, device_ip, device_id): + def validate_protocol_version(self, device_addr, device_id): LOGGER.info(f'Resolving protocol version for BACnet device: {device_id}') try: version = self.bacnet.read( - f'{device_ip} device {device_id} protocolVersion') + f'{device_addr} device {device_id} protocolVersion') revision = self.bacnet.read( - f'{device_ip} device {device_id} protocolRevision') + f'{device_addr} device {device_id} protocolRevision') protocol_version = f'{version}.{revision}' result = True result_description = f'Device uses BACnet version {protocol_version}' @@ -120,6 +121,9 @@ def validate_bacnet_source(self, object_id, device_hw_addr): capture_file = os.path.join(self._captures_dir, self._capture_file) packets = self.get_bacnet_packets(capture_file, object_id) valid = None + # If no packets are found in protocol.pcap + if not packets: + LOGGER.debug(f'No BACnet packets found for object id {object_id}') for packet in packets: if object_id in packet['_source']['layers']['bacapp.instance_number']: if device_hw_addr.lower() in packet['_source']['layers']['eth.src']: @@ -136,7 +140,7 @@ def validate_bacnet_source(self, object_id, device_hw_addr): valid = False return valid except Exception: # pylint: disable=W0718 - LOGGER.error('Error occured when validating source', exc_info=True) + LOGGER.error('Error occurred when validating source', exc_info=True) return False def get_bacnet_packets(self, capture_file, object_id): diff --git a/modules/test/protocol/python/src/protocol_modbus.py b/modules/test/protocol/python/src/protocol_modbus.py index 925e9517a..a722f928e 100644 --- a/modules/test/protocol/python/src/protocol_modbus.py +++ b/modules/test/protocol/python/src/protocol_modbus.py @@ -103,7 +103,7 @@ def __init__(self, log, device_ip, config): self._discrete_input_enabled = False # Initialize the modbus client - self.client = ModbusClient(device_ip, self._port) + self.client = ModbusClient(host=device_ip, port=self._port) # Connections created from this method are simple socket connections # and aren't indicative of valid modbus diff --git a/modules/test/protocol/python/src/protocol_module.py b/modules/test/protocol/python/src/protocol_module.py index 4f7c1a7e7..92891465d 100644 --- a/modules/test/protocol/python/src/protocol_module.py +++ b/modules/test/protocol/python/src/protocol_module.py @@ -22,14 +22,14 @@ class ProtocolModule(TestModule): - """Protocol Test module""" + """Protocol test module""" def __init__(self, module): self._supports_bacnet = False super().__init__(module_name=module, log_name=LOG_NAME) global LOGGER LOGGER = self._get_logger() - self._bacnet = BACnet(log=LOGGER,device_hw_addr=self._device_mac) + self._bacnet = BACnet(log=LOGGER, device_hw_addr=self._device_mac) def _protocol_valid_bacnet(self): LOGGER.info('Running protocol.valid_bacnet') @@ -64,19 +64,20 @@ def _protocol_bacnet_version(self): result_status = 'Feature Not Detected' result_description = 'Device did not respond to BACnet discovery' + LOGGER.debug(f'BACnet supported: {self._supports_bacnet}') + # Do not run test if device does not support BACnet if not self._supports_bacnet: return result_status, result_description if len(self._bacnet.devices) > 0: for device in self._bacnet.devices: - if self._device_ipv4_addr in device[2]: - LOGGER.debug(f'Checking BACnet version for device: {device}') - result_status, result_description = \ - self._bacnet.validate_protocol_version(device[2], device[3]) - break - else: - LOGGER.debug('Device does not match expected IP address, skipping') + LOGGER.debug(f'Checking BACnet version for device: {device}') + device_addr = device[2] + device_id = device[3] + result_status, result_description = \ + self._bacnet.validate_protocol_version(device_addr,device_id) + break LOGGER.info(result_description) return result_status, result_description diff --git a/modules/test/protocol/python/src/run.py b/modules/test/protocol/python/src/run.py index d47c81cb6..a2788c833 100644 --- a/modules/test/protocol/python/src/run.py +++ b/modules/test/protocol/python/src/run.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Run Baseline module""" +"""Run protocol module""" import argparse import signal import sys diff --git a/modules/test/services/README.md b/modules/test/services/README.md index eae8a0bd0..f6133cc6e 100644 --- a/modules/test/services/README.md +++ b/modules/test/services/README.md @@ -22,6 +22,6 @@ Within the ```python/src``` directory, the below tests are executed. | security.services.pop | Check POP ports 109 and 110 are disabled and POP is not running on any port | There is no POP service running on any port | Required | | security.services.imap | Check IMAP port 143 is disabled and IMAP is not running on any port | There is no IMAP service running on any port | Required | | security.services.snmpv3 | Check SNMP port 161/162 is disabled. If SNMP is an essential service, it should be v3 | Device is unreachable on port 161/162 unless SNMP is essential in which case it is SNMPv3 that is used | Required | -| security.services.vnc | Check VNS is disabled on any port | Device cannot be accessed via VNC on any port | Required | +| security.services.vnc | Check VNC is disabled on any port | Device cannot be accessed via VNC on any port | Required | | security.services.tftp | Check TFTP port 69 is disabled (UDP) | There is no TFTP service running on any port | Required | | ntp.network.ntp_server | Check NTP port 123 is disabled and the device is not acting as an NTP server | The devices does not respond to NTP requests | Required | \ No newline at end of file diff --git a/modules/test/services/bin/start_test_module b/modules/test/services/bin/start_test_module index d8cede486..a42ee4cf0 100644 --- a/modules/test/services/bin/start_test_module +++ b/modules/test/services/bin/start_test_module @@ -41,11 +41,8 @@ else fi # Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python scrip that will execute the tests for this module diff --git a/modules/test/services/conf/module_config.json b/modules/test/services/conf/module_config.json index 5c20b4beb..b37435eda 100644 --- a/modules/test/services/conf/module_config.json +++ b/modules/test/services/conf/module_config.json @@ -9,14 +9,13 @@ "docker": { "depends_on": "base", "enable_container": true, - "timeout": 600 + "timeout": 900 }, "tests": [ { "name": "security.services.ftp", "test_description": "Check FTP port 20/21 is disabled and FTP is not running on any port", "expected_behavior": "There is no FTP service running on any port", - "required_result": "Required", "config": { "services": [ "ftp", @@ -50,7 +49,6 @@ "name": "security.ssh.version", "test_description": "If the device is running a SSH server ensure it is SSHv2", "expected_behavior": "SSH server is not running or server is SSHv2", - "required_result": "Required", "config": { "services": ["ssh"], "ports": [ @@ -70,7 +68,6 @@ "name": "security.services.telnet", "test_description": "Check TELNET port 23 is disabled and TELNET is not running on any port", "expected_behavior": "There is no Telnet service running on any port", - "required_result": "Required", "config": { "services": [ "telnet" @@ -95,7 +92,6 @@ "name": "security.services.smtp", "test_description": "Check SMTP ports 25, 465 and 587 are not enabled and SMTP is not running on any port.", "expected_behavior": "There is no SMTP service running on any port", - "required_result": "Required", "config": { "services": [ "smtp" @@ -123,7 +119,6 @@ "name": "security.services.http", "test_description": "Check that there is no HTTP server running on any port", "expected_behavior": "Device is unreachable on port 80 (or any other port) and only responds to HTTPS requests on port 443 (or any other port if HTTP is used at all)", - "required_result": "Required", "config": { "services": [ "http" @@ -158,7 +153,6 @@ "name": "security.services.pop", "test_description": "Check POP ports 109 and 110 are disabled and POP is not running on any port", "expected_behavior": "There is no POP service running on any port", - "required_result": "Required", "config": { "services": [ "pop2", @@ -200,7 +194,6 @@ "name": "security.services.imap", "test_description": "Check IMAP port 143 is disabled and IMAP is not running on any port", "expected_behavior": "There is no IMAP service running on any port", - "required_result": "Required", "config": { "services": [ "imap", @@ -250,7 +243,6 @@ "name": "security.services.snmpv3", "test_description": "Check SNMP port 161/162 is disabled. If SNMP is an essential service, check it supports version 3", "expected_behavior": "Device is unreachable on port 161 (or any other port) and device is unreachable on port 162 (or any other port) unless SNMP is essential in which case it is SNMPv3 is used.", - "required_result": "Required", "config": { "services": [ "snmp" @@ -275,7 +267,6 @@ "name": "security.services.vnc", "test_description": "Check VNC is disabled on any port", "expected_behavior": "Device cannot be accessed / connected to via VNC on any port", - "required_result": "Required", "config": { "services": [ "vnc", @@ -319,6 +310,10 @@ { "number": 5903, "type": "tcp" + }, + { + "number": 6001, + "type": "tcp" } ] }, @@ -330,7 +325,6 @@ "name": "security.services.tftp", "test_description": "Check TFTP port 69 is disabled (UDP)", "expected_behavior": "There is no TFTP service running on any port", - "required_result": "Required", "config": { "services": [ "tftp", @@ -363,7 +357,6 @@ "name": "ntp.network.ntp_server", "test_description": "Check NTP port 123 is disabled and the device is not operating as an NTP server", "expected_behavior": "The device does not respond to NTP requests when it's IP is set as the NTP server on another device", - "required_result": "Required", "config": { "services": [ "ntp" @@ -379,6 +372,22 @@ "Disable the NTP server", "Drop traffic entering port 123/udp" ] + }, + { + "name": "protocol.services.bacnet", + "test_description": "Report whether the device is running a BACnet server", + "expected_behavior": "The device may or may not be running a BACnet server", + "config": { + "services": [ + "bacnet" + ], + "ports": [ + { + "number": 47808, + "type": "udp" + } + ] + } } ] } diff --git a/modules/test/services/python/requirements.txt b/modules/test/services/python/requirements.txt index a3fdd1857..02acc1e19 100644 --- a/modules/test/services/python/requirements.txt +++ b/modules/test/services/python/requirements.txt @@ -1 +1,6 @@ -xmltodict==0.13.0 \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies + +# User defined packages +xmltodict==0.14.2 diff --git a/modules/test/services/python/src/services_module.py b/modules/test/services/python/src/services_module.py index bfa232c87..06ceed67d 100644 --- a/modules/test/services/python/src/services_module.py +++ b/modules/test/services/python/src/services_module.py @@ -19,26 +19,26 @@ import xmltodict from test_module import TestModule import os +from jinja2 import Environment, FileSystemLoader LOG_NAME = 'test_services' -MODULE_REPORT_FILE_NAME = 'services_report.html' +MODULE_REPORT_FILE_NAME = 'services_report.j2.html' NMAP_SCAN_RESULTS_SCAN_FILE = 'services_scan_results.json' LOGGER = None +REPORT_TEMPLATE_FILE = 'report_template.jinja2' class ServicesModule(TestModule): """Services Test module""" - def __init__(self, + def __init__(self, # pylint: disable=R0917 module, - log_dir=None, conf_file=None, results_dir=None, run=True, nmap_scan_results_path=None): super().__init__(module_name=module, log_name=LOG_NAME, - log_dir=log_dir, conf_file=conf_file, results_dir=results_dir) self._scan_tcp_results = None @@ -54,6 +54,26 @@ def __init__(self, self._run_nmap() def generate_module_report(self): + # Load Jinja2 template + loader=FileSystemLoader(self._report_template_folder) + template = Environment( + loader=loader, + trim_blocks=True, + lstrip_blocks=True + ).get_template(REPORT_TEMPLATE_FILE) + module_header = 'Services Module' + summary_headers = [ + 'TCP ports open', + 'UDP ports open', + 'Total ports open', + ] + module_data_headers = [ + 'Port', + 'State', + 'Service', + 'Version', + ] + # Use os.path.join to create the complete file path nmap_scan_results_file = os.path.join(self._nmap_scan_results_path, NMAP_SCAN_RESULTS_SCAN_FILE) @@ -83,65 +103,32 @@ def generate_module_report(self): else: udp_open += 1 - html_content = '

Services Module

' - - # Add summary table - html_content += (f''' - - - - - - - - - - - - - - -
TCP ports openUDP ports openTotal ports open
{tcp_open}{udp_open}{tcp_open + udp_open}
- ''') + summary_data = [ + tcp_open, + udp_open, + tcp_open + udp_open, + ] + module_data = [] if (tcp_open + udp_open) > 0: - - table_content = ''' - - - - - - - - - - ''' - for row in nmap_table_data: - - table_content += (f''' - - - - - - ''') - - table_content += ''' - -
PortStateServiceVersion
{row['Port']}/{row['Type']}{row['State']}{row['Service']}{row['Version']}
- ''' - - html_content += table_content - - else: - - html_content += (''' -
-
- No open ports detected -
''') + port = row['Port'] + type_ = row['Type'] + module_data.append({ + 'Port': f'{port}/{type_}', + 'State': row['State'], + 'Service': row['Service'], + 'Version': row['Version'], + }) + + html_content = template.render( + base_template=self._base_template_file, + module_header=module_header, + summary_headers=summary_headers, + summary_data=summary_data, + module_data_headers=module_data_headers, + module_data=module_data, + ) LOGGER.debug('Module report:\n' + html_content) @@ -158,6 +145,8 @@ def generate_module_report(self): def _run_nmap(self): LOGGER.info('Running nmap') + self._device_ipv4_addr = self._get_device_ipv4() + LOGGER.info('Resolved device IP: ' + str(self._device_ipv4_addr)) # Run the monitor method asynchronously to keep this method non-blocking self._tcp_scan_thread = threading.Thread(target=self._scan_tcp_ports) @@ -198,14 +187,15 @@ def _process_port_results(self): self._scan_results.update(self._scan_udp_results) def _scan_tcp_ports(self): - max_port = 1000 - LOGGER.info('Running nmap TCP port scan') - nmap_results = util.run_command( - f'''nmap --open -sT -sV -Pn -v -p 1-{max_port} - --version-intensity 7 -T4 -oX - {self._ipv4_addr}''')[0] + LOGGER.info(f'Running nmap TCP port scan for {self._device_ipv4_addr}') + nmap_results = util.run_command( # pylint: disable=E1120 + f'''nmap --open -sT -sV -Pn -v -p 1-65535 + --version-intensity 7 -T4 -oX - {self._device_ipv4_addr}''')[0] LOGGER.info('TCP port scan complete') + LOGGER.debug(f'TCP Scan results raw: {nmap_results}') nmap_results_json = self._nmap_results_to_json(nmap_results) + LOGGER.debug(f'TCP Scan results JSON: {nmap_results_json}') self._scan_tcp_results = self._process_nmap_json_results( nmap_results_json=nmap_results_json) @@ -226,12 +216,14 @@ def _scan_udp_ports(self): if len(ports) > 0: port_list = ','.join(ports) - LOGGER.info('Running nmap UDP port scan') - LOGGER.debug('UDP ports: ' + str(port_list)) - nmap_results = util.run_command( - f'nmap -sU -sV -p {port_list} -oX - {self._ipv4_addr}')[0] + LOGGER.info(f'Running nmap UDP port scan for {self._device_ipv4_addr}') + LOGGER.info('UDP ports: ' + str(port_list)) + nmap_results = util.run_command( # pylint: disable=E1120 + f'nmap -sU -sV -p {port_list} -oX - {self._device_ipv4_addr}')[0] LOGGER.info('UDP port scan complete') + LOGGER.debug(f'UDP Scan results raw: {nmap_results}') nmap_results_json = self._nmap_results_to_json(nmap_results) + LOGGER.debug(f'UDP Scan results JSON: {nmap_results_json}') self._scan_udp_results = self._process_nmap_json_results( nmap_results_json=nmap_results_json) @@ -265,12 +257,14 @@ def _json_port_to_dict(self, port_json): port['number'] = port_json['@portid'] port['tcp_udp'] = port_json['@protocol'] port['state'] = port_json['state']['@state'] - port['service'] = port_json['service']['@name'] + port['service'] = 'unknown' port['version'] = '' - if '@version' in port_json['service']: - port['version'] += port_json['service']['@version'] - if '@extrainfo' in port_json['service']: - port['version'] += ' ' + port_json['service']['@extrainfo'] + if 'service' in port_json: + port['service'] = port_json['service']['@name'] + if '@version' in port_json['service']: + port['version'] += port_json['service']['@version'] + if '@extrainfo' in port_json['service']: + port['version'] += ' ' + port_json['service']['@extrainfo'] port_result = {port_json['@portid'] + port['tcp_udp']: port} return port_result @@ -423,3 +417,15 @@ def _security_ssh_version(self, config): else: return (False, f"SSH server found running {open_port_info['version']}") + + def _protocol_services_bacnet(self, config): + LOGGER.info('Running protocol.services.bacnet') + + open_ports = self._check_results(config['ports'], config['services']) + if len(open_ports) == 0: + return False, 'No BACnet server found' + else: + return ( + True, + f'''Found BACnet server running on port {', '.join(open_ports)}''' + ) diff --git a/modules/test/services/resources/report_template.jinja2 b/modules/test/services/resources/report_template.jinja2 new file mode 100644 index 000000000..c9b1438ae --- /dev/null +++ b/modules/test/services/resources/report_template.jinja2 @@ -0,0 +1,29 @@ +{% extends base_template %} +{% block content %} +{% if module_data %} + + + + {% for header in module_data_headers %} + + {% endfor %} + + + + {% for row in module_data %} + + + + + + + {% endfor %} + +
{{ header }}
{{ row['Port'] }}{{ row['State'] }}{{ row['Service'] }}{{ row['Version'] }}
+{% else %} +
+
+ No open ports detected +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/modules/test/services/services.Dockerfile b/modules/test/services/services.Dockerfile index 3a89fc33c..1277c2f8d 100644 --- a/modules/test/services/services.Dockerfile +++ b/modules/test/services/services.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/services-test -FROM test-run/base-test:latest +# Image name: testrun/services-test +FROM testrun/base-test:latest ARG MODULE_NAME=services ARG MODULE_DIR=modules/test/$MODULE_NAME @@ -22,7 +22,7 @@ ARG MODULE_DIR=modules/test/$MODULE_NAME COPY $MODULE_DIR/python/requirements.txt /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf @@ -31,4 +31,7 @@ COPY $MODULE_DIR/conf /testrun/conf COPY $MODULE_DIR/bin /testrun/bin # Copy over all python files -COPY $MODULE_DIR/python /testrun/python \ No newline at end of file +COPY $MODULE_DIR/python /testrun/python + +# Copy Jinja template +COPY $MODULE_DIR/resources/report_template.jinja2 $REPORT_TEMPLATE_PATH/ \ No newline at end of file diff --git a/modules/test/tls/README.md b/modules/test/tls/README.md index ba1c6b1db..f0e0119af 100644 --- a/modules/test/tls/README.md +++ b/modules/test/tls/README.md @@ -14,5 +14,9 @@ Within the ```python/src``` directory, the below tests are executed. | ID | Description | Expected behavior | Required result |---|---|---|---| -| security.tls.v1_2_server | Check the device web server is TLSv1.2 minimum and the certificate is valid | TLS 1.2 certificate is issues to the client when accessed | Required | -| security.tls.v1_2_client | Device uses TLS with connections to external services on any port | The packet indicates a TLS connection with at least TLS v1.2 and support for ECDH and ECDSA ciphers | Required | \ No newline at end of file +| security.tls.v1_0_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS 1.0 and support | Informational | +| security.tls.v1_2_server | Check the device web server TLS 1.2 and the certificate is valid | TLS 1.2 certificate is issues to the web browser client when accessed | Required if Applicable | +| security.tls.v1_2_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS v1.2 and support for ECDH and ECDSA ciphers | Required if Applicable | +| security.tls.v1_3_server | Check the device web server TLS 1.3 and the certificate is valid | TLS 1.3 certificate is issued to the web browser client when accessed | Informational | +| security.tls.v1_3_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS 1.3 | Informational | + diff --git a/modules/test/tls/bin/get_client_hello_packets.sh b/modules/test/tls/bin/get_client_hello_packets.sh index d563d11f2..fd06a3d28 100755 --- a/modules/test/tls/bin/get_client_hello_packets.sh +++ b/modules/test/tls/bin/get_client_hello_packets.sh @@ -15,19 +15,27 @@ # limitations under the License. CAPTURE_FILE="$1" -SRC_IP="$2" +SRC_MAC="$2" TLS_VERSION="$3" TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" -TSHARK_FILTER="ssl.handshake.type==1 and ip.src==$SRC_IP" +TSHARK_FILTER="ssl.handshake.type==1 and eth.src==$SRC_MAC" -if [[ $TLS_VERSION == '1.2' || -z $TLS_VERSION ]];then +if [[ $TLS_VERSION == '1.0' ]]; then + TSHARK_FILTER="$TSHARK_FILTER and ssl.handshake.version==0x0301" +elif [[ $TLS_VERSION == '1.1' ]]; then + TSHARK_FILTER="$TSHARK_FILTER and ssl.handshake.version==0x0302" +elif [[ $TLS_VERSION == '1.2' || -z $TLS_VERSION ]]; then TSHARK_FILTER="$TSHARK_FILTER and ssl.handshake.version==0x0303" -elif [ $TLS_VERSION == '1.3' ];then +elif [[ $TLS_VERSION == '1.3' ]]; then TSHARK_FILTER="$TSHARK_FILTER and (ssl.handshake.version==0x0304 or tls.handshake.extensions.supported_version==0x0304)" +else + echo "Unsupported TLS version: $TLS_VERSION" + exit 1 fi response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) echo "$response" + \ No newline at end of file diff --git a/modules/test/tls/bin/get_handshake_complete.sh b/modules/test/tls/bin/get_handshake_complete.sh index 9bf9c525d..b36997f6d 100755 --- a/modules/test/tls/bin/get_handshake_complete.sh +++ b/modules/test/tls/bin/get_handshake_complete.sh @@ -1,33 +1,39 @@ -#!/bin/bash - -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -CAPTURE_FILE="$1" -SRC_IP="$2" -DST_IP="$3" -TLS_VERSION="$4" - -TSHARK_FILTER="ip.src==$SRC_IP and ip.dst==$DST_IP " - -if [[ $TLS_VERSION == '1.2' || -z $TLS_VERSION ]];then - TSHARK_FILTER=$TSHARK_FILTER " and ssl.handshake.type==2 and tls.handshake.type==14 " -elif [ $TLS_VERSION == '1.2' ];then - TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.type==2 and tls.handshake.extensions.supported_version==0x0304" -fi - -response=$(tshark -r "$CAPTURE_FILE" $TSHARK_FILTER) - -echo "$response" - \ No newline at end of file +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +CAPTURE_FILE="$1" +SRC_IP="$2" +DST_IP="$3" +TLS_VERSION="$4" + +TSHARK_FILTER="ip.src==$SRC_IP and ip.dst==$DST_IP" + +if [[ $TLS_VERSION == '1.0' ]]; then + TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.type==2 and tls.handshake.type==14" +elif [[ $TLS_VERSION == '1.1' ]]; then + TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.type==2 and tls.handshake.type==14" +elif [[ $TLS_VERSION == '1.2' || -z $TLS_VERSION ]]; then + TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.type==2 and tls.handshake.type==14" +elif [[ $TLS_VERSION == '1.3' ]]; then + TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.type==2 and tls.handshake.extensions.supported_version==0x0304" +else + echo "Unsupported TLS version: $TLS_VERSION" + exit 1 +fi + +response=$(tshark -r "$CAPTURE_FILE" $TSHARK_FILTER) + +echo "$response" diff --git a/modules/test/tls/bin/get_non_tls_client_connections.sh b/modules/test/tls/bin/get_non_tls_client_connections.sh index 2bfc3d635..03fe2a393 100755 --- a/modules/test/tls/bin/get_non_tls_client_connections.sh +++ b/modules/test/tls/bin/get_non_tls_client_connections.sh @@ -15,7 +15,7 @@ # limitations under the License. CAPTURE_FILE="$1" -SRC_IP="$2" +SRC_MAC="$2" TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" # Filter out TLS, DNS and NTP, ICMP (ping), braodcast and multicast packets @@ -24,9 +24,8 @@ TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" # - Multicast and braodcast protocols are not typically encrypted so we aren't expecting them to # be over TLS connections # - ICMP (ping) requests are not encrypted so we also need to ignore these -TSHARK_FILTER="ip.src == $SRC_IP and not tls and not dns and not ntp and not icmp and not(ip.dst == 224.0.0.0/4 or ip.dst == 255.255.255.255)" +TSHARK_FILTER="eth.src == $SRC_MAC and not tls and not dns and not ntp and not icmp and not(ip.dst == 224.0.0.0/4 or ip.dst == 255.255.255.255)" response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) echo "$response" - \ No newline at end of file diff --git a/modules/test/tls/bin/get_tls_client_connections.sh b/modules/test/tls/bin/get_tls_client_connections.sh index e2e6da91b..8a0e1ddab 100755 --- a/modules/test/tls/bin/get_tls_client_connections.sh +++ b/modules/test/tls/bin/get_tls_client_connections.sh @@ -1,32 +1,31 @@ -#!/bin/bash - -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -CAPTURE_FILE="$1" -SRC_IP="$2" -PROTOCOL=$3 - -TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" -TSHARK_FILTER="ip.src == $SRC_IP and tls" - -# Add a protocol filter if defined -if [ -n "$PROTOCOL" ];then - TSHARK_FILTER="$TSHARK_FILTER and $PROTOCOL" -fi - -response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) - -echo "$response" - \ No newline at end of file +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +CAPTURE_FILE="$1" +SRC_MAC="$2" +PROTOCOL=$3 + +TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" +TSHARK_FILTER="eth.src == $SRC_MAC and tls" + +# Add a protocol filter if defined +if [ -n "$PROTOCOL" ];then + TSHARK_FILTER="$TSHARK_FILTER and $PROTOCOL" +fi + +response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) + +echo "$response" diff --git a/modules/test/tls/bin/get_tls_packets.sh b/modules/test/tls/bin/get_tls_packets.sh index e64d4e9fb..7498d8e62 100755 --- a/modules/test/tls/bin/get_tls_packets.sh +++ b/modules/test/tls/bin/get_tls_packets.sh @@ -16,13 +16,13 @@ CAPTURE_FILE="$1" -SRC_IP="$2" +SRC_MAC="$2" TLS_VERSION="$3" -TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" +TSHARK_OUTPUT="-T json -e eth.src -e tcp.dstport -e ip.dst" # Handshakes will still report TLS version 1 even for TLS 1.2 connections # so we need to filter thes out -TSHARK_FILTER="ip.src==$SRC_IP and ssl.handshake.type!=1" +TSHARK_FILTER="eth.src==$SRC_MAC and ssl.handshake.type!=1" if [ $TLS_VERSION == '1.0' ];then TSHARK_FILTER="$TSHARK_FILTER and ssl.record.version==0x0301" @@ -37,4 +37,3 @@ fi response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) echo "$response" - \ No newline at end of file diff --git a/modules/test/tls/bin/start_test_module b/modules/test/tls/bin/start_test_module index d8cede486..a42ee4cf0 100644 --- a/modules/test/tls/bin/start_test_module +++ b/modules/test/tls/bin/start_test_module @@ -41,11 +41,8 @@ else fi # Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python scrip that will execute the tests for this module diff --git a/modules/test/tls/conf/module_config.json b/modules/test/tls/conf/module_config.json index cd77f8299..228fbb64d 100644 --- a/modules/test/tls/conf/module_config.json +++ b/modules/test/tls/conf/module_config.json @@ -9,14 +9,22 @@ "docker": { "depends_on": "base", "enable_container": true, - "timeout": 300 + "timeout": 540 }, "tests":[ + { + "name": "security.tls.v1_0_client", + "test_description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", + "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.0 and support", + "recommendations": [ + "Disable connections to unsecure services", + "Ensure any URLs connected to are secure (https)" + ] + }, { "name": "security.tls.v1_2_server", - "test_description": "Check the device web server TLS 1.2 & certificate is valid", + "test_description": "Check the device web server TLS 1.2 and the certificate is valid", "expected_behavior": "TLS 1.2 certificate is issued to the web browser client when accessed", - "required_result": "Required if Applicable", "recommendations": [ "Enable TLS 1.2 support in the web server configuration", "Disable TLS 1.0 and 1.1", @@ -27,7 +35,25 @@ "name": "security.tls.v1_2_client", "test_description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers", - "required_result": "Required if Applicable", + "recommendations": [ + "Disable connections to unsecure services", + "Ensure any URLs connected to are secure (https)" + ] + }, + { + "name": "security.tls.v1_3_server", + "test_description": "Check the device web server TLS 1.3 and the certificate is valid", + "expected_behavior": "TLS 1.3 certificate is issued to the web browser client when accessed", + "recommendations": [ + "Enable TLS 1.3 support in the web server configuration", + "Disable TLS 1.0 and 1.1", + "Sign the certificate used by the web server" + ] + }, + { + "name": "security.tls.v1_3_client", + "test_description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", + "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.3", "recommendations": [ "Disable connections to unsecure services", "Ensure any URLs connected to are secure (https)" diff --git a/modules/test/tls/python/requirements-test.txt b/modules/test/tls/python/requirements-test.txt new file mode 100644 index 000000000..10f9fa8d1 --- /dev/null +++ b/modules/test/tls/python/requirements-test.txt @@ -0,0 +1,6 @@ +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies + +# User defined packages +scapy==2.6.0 diff --git a/modules/test/tls/python/requirements.txt b/modules/test/tls/python/requirements.txt index 7624a2c68..2cbc1db46 100644 --- a/modules/test/tls/python/requirements.txt +++ b/modules/test/tls/python/requirements.txt @@ -1,5 +1,21 @@ -cryptography==42.0.4 # Do not upgrade until TLS module can be fixed to account for removed x509 property in version 39 -pyOpenSSL==24.1.0 -lxml==5.1.0 # Requirement of pyshark but if upgraded automatically above 5.1 will cause a python crash -pyshark==0.6 -requests==2.32.0 +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +appdirs==1.4.4 +certifi==2024.8.30 +cffi==1.17.1 +charset-normalizer==3.3.2 +idna==3.8 +packaging==24.1 +pycparser==2.22 +pyshark==0.6 +termcolor==2.4.0 +urllib3==2.2.2 + +# User defined packages +cryptography==44.0.1 +pyOpenSSL==24.3.0 +lxml==5.1.0 # Requirement of pyshark but if upgraded automatically above 5.1 will cause a +pyshark==0.6 +requests==2.32.3 +python-nmap==0.7.1 diff --git a/modules/test/tls/python/src/http_scan.py b/modules/test/tls/python/src/http_scan.py new file mode 100644 index 000000000..a25f5215d --- /dev/null +++ b/modules/test/tls/python/src/http_scan.py @@ -0,0 +1,76 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Module that contains various methods for scaning for HTTP/HTTPS services""" +import nmap +import socket +import ssl + +LOGGER = None + + +class HTTPScan(): + """Helper class to scan for all HTTP/HTTPS services for a device""" + + def __init__(self, logger): + global LOGGER + LOGGER = logger + + def scan_all_ports(self, ip): + """Scans all ports and identifies potential HTTP/HTTPS ports.""" + nm = nmap.PortScanner() + nm.scan(hosts=ip, ports='1-65535', arguments='--open -sV') + + http_ports = [] + for host in nm.all_hosts(): + for proto in nm[host].all_protocols(): + for port in nm[host][proto].keys(): + service = nm[host][proto][port]['name'] + if 'http' in service: + http_ports.append(port) + return http_ports + + def is_https(self, ip, port): + """Attempts a TLS handshake to determine if the port serves HTTPS.""" + try: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + with socket.create_connection((ip, port), timeout=2) as sock: + with context.wrap_socket(sock, server_hostname=ip): + return True + except ssl.SSLError: + return False + except Exception: # pylint: disable=W0718 + return False + + def verify_http_or_https(self, ip, ports): + """Classifies each port as HTTP or HTTPS.""" + results = {} + for port in ports: + if self.is_https(ip, port): + results[port] = 'HTTPS' + else: + results[port] = 'HTTP' + return results + + def scan_for_http_services(self, ip_address): + LOGGER.info(f'Scanning for HTTP ports on {ip_address}') + http_ports = self.scan_all_ports(ip_address) + results = None + if len(http_ports) > 0: + LOGGER.info(f'Checking HTTP ports on {ip_address}: {http_ports}') + results = self.verify_http_or_https(ip_address, http_ports) + for port, service_type in results.items(): + LOGGER.info(f'Port {port}: {service_type}') + return results diff --git a/modules/test/tls/python/src/run.py b/modules/test/tls/python/src/run.py index 89de9f65e..2b7ea7e0f 100644 --- a/modules/test/tls/python/src/run.py +++ b/modules/test/tls/python/src/run.py @@ -37,7 +37,7 @@ def __init__(self, module): self._test_module = TLSModule(module) self._test_module.run_tests() - #self._test_module.generate_module_report() + self._test_module.generate_module_report() def _handler(self, signum): LOGGER.debug('SigtermEnum: ' + str(signal.SIGTERM)) diff --git a/modules/test/tls/python/src/tls_module.py b/modules/test/tls/python/src/tls_module.py index 9aab1b782..3ccd96b59 100644 --- a/modules/test/tls/python/src/tls_module.py +++ b/modules/test/tls/python/src/tls_module.py @@ -12,26 +12,37 @@ # See the License for the specific language governing permissions and # limitations under the License. """TLS test module""" +# pylint: disable=W0212 + from test_module import TestModule from tls_util import TLSUtil +from http_scan import HTTPScan +import os import pyshark +from binascii import hexlify from cryptography import x509 from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa, dsa, ec +from cryptography.x509 import AuthorityKeyIdentifier, SubjectKeyIdentifier, BasicConstraints, KeyUsage +from cryptography.x509 import GeneralNames, DNSName, ExtendedKeyUsage, ObjectIdentifier, SubjectAlternativeName +from jinja2 import Environment, FileSystemLoader LOG_NAME = 'test_tls' -MODULE_REPORT_FILE_NAME = 'tls_report.html' +MODULE_REPORT_FILE_NAME = 'tls_report.j2.html' STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap' MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap' TLS_CAPTURE_FILE = '/runtime/output/tls.pcap' GATEWAY_CAPTURE_FILE = '/runtime/network/gateway.pcap' LOGGER = None +REPORT_TEMPLATE_FILE = 'report_template.jinja2' + class TLSModule(TestModule): """The TLS testing module.""" - def __init__(self, + def __init__(self, # pylint: disable=R0917 module, - log_dir=None, conf_file=None, results_dir=None, startup_capture_file=STARTUP_CAPTURE_FILE, @@ -39,7 +50,6 @@ def __init__(self, tls_capture_file=TLS_CAPTURE_FILE): super().__init__(module_name=module, log_name=LOG_NAME, - log_dir=log_dir, conf_file=conf_file, results_dir=results_dir) self.startup_capture_file = startup_capture_file @@ -48,155 +58,241 @@ def __init__(self, global LOGGER LOGGER = self._get_logger() self._tls_util = TLSUtil(LOGGER) - - # def generate_module_report(self): - # html_content = '

TLS Module

' - - # # List of capture files to scan - # pcap_files = [ - # self.startup_capture_file, self.monitor_capture_file, - # self.tls_capture_file - # ] - # certificates = self.extract_certificates_from_pcap(pcap_files, - # self._device_mac) - # if len(certificates) > 0: - - # # Add summary table - # summary_table = ''' - # - # - # - # - # - # - # - # - # - # - # - # ''' - - # # table_content = ''' - # #
ExpiryLengthTypePort numberSigned by
- # # - # # - # # - # # - # # - # # - # # - # # - # # - # # ''' - - # cert_tables = [] - # for cert_num, ((ip_address, port), cert) in enumerate( - # certificates.items()): - - # # Extract certificate data - # not_valid_before = cert.not_valid_before - # not_valid_after = cert.not_valid_after - # version_value = f'{cert.version.value + 1} ({hex(cert.version.value)})' - # signature_alg_value = cert.signature_algorithm_oid._name # pylint: disable=W0212 - # not_before = str(not_valid_before) - # not_after = str(not_valid_after) - # public_key = cert.public_key() - # signed_by = 'None' - # if isinstance(public_key, rsa.RSAPublicKey): - # public_key_type = 'RSA' - # elif isinstance(public_key, dsa.DSAPublicKey): - # public_key_type = 'DSA' - # elif isinstance(public_key, ec.EllipticCurvePublicKey): - # public_key_type = 'EC' - # else: - # public_key_type = 'Unknown' - # # Calculate certificate length - # cert_length = len(cert.public_bytes( - # encoding=serialization.Encoding.DER)) - - # # Generate the Certificate table - # # cert_table = (f'| Property | Value |\n' - # # f'|---|---|\n' - # # f"| {'Version':<17} | {version_value:^25} |\n" - # # f"| {'Signature Alg.':<17} | - # {signature_alg_value:^25} |\n" - # # f"| {'Validity from':<17} | {not_before:^25} |\n" - # # f"| {'Valid to':<17} | {not_after:^25} |") - - # # Generate the Subject table - # subj_table = ('| Distinguished Name | Value |\n' - # '|---|---|') - # for val in cert.subject.rdns: - # dn = val.rfc4514_string().split('=') - # subj_table += f'\n| {dn[0]} | {dn[1]}' - - # # Generate the Issuer table - # iss_table = ('| Distinguished Name | Value |\n' - # '|---|---|') - # for val in cert.issuer.rdns: - # dn = val.rfc4514_string().split('=') - # iss_table += f'\n| {dn[0]} | {dn[1]}' - # if 'CN' in dn[0]: - # signed_by = dn[1] - - # ext_table = None - # # if cert.extensions: - # # ext_table = ('| Extension | Value |\n' - # # '|---|---|') - # # for extension in cert.extensions: - # # for extension_value in extension.value: - # # ext_table += f'''\n| {extension.oid._name} | - # # {extension_value.value}''' # pylint: disable=W0212 - # # cert_table = f'### Certificate\n{cert_table}' - # # cert_table += f'\n\n### Subject\n{subj_table}' - # # cert_table += f'\n\n### Issuer\n{iss_table}' - # # if ext_table is not None: - # # cert_table += f'\n\n### Extensions\n{ext_table}' - # # cert_tables.append(cert_table) - - # summary_table += f''' - # - # - # - # - # - # - # - # ''' - - # summary_table += ''' - # - #
ExpiryLengthTypePort numberSigned by
{not_after}{cert_length}{public_key_type}{port}{signed_by}
- # ''' - - # html_content += summary_table - - # else: - # html_content += (''' - #
- #
- # No TLS certificates found on the device - #
''') - - # LOGGER.debug('Module report:\n' + html_content) - - # # Use os.path.join to create the complete file path - # report_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME) - - # # Write the content to a file - # with open(report_path, 'w', encoding='utf-8') as file: - # file.write(html_content) - - # LOGGER.info('Module report generated at: ' + str(report_path)) - # return report_path + self._http_scan = HTTPScan(LOGGER) + self._scan_results = None + + def generate_module_report(self): + # Load Jinja2 template + loader=FileSystemLoader(self._report_template_folder) + template = Environment( + loader=loader, + trim_blocks=True, + lstrip_blocks=True + ).get_template(REPORT_TEMPLATE_FILE) + module_header='TLS Module' + # Summary table headers + summary_headers = [ + 'Expiry', + 'Length', + 'Type', + 'Port number', + 'Signed by', + ] + # Cert table headers + cert_table_headers = ['Property', 'Value'] + # Outbound connections table headers + outbound_headers = ['Destination IP', 'Port'] + pages = {} + outbound_conns = None + + # List of capture files to scan + pcap_files = [ + self.startup_capture_file, self.monitor_capture_file, + self.tls_capture_file + ] + certificates = self.extract_certificates_from_pcap(pcap_files, + self._device_mac) + if len(certificates) > 0: + + # pylint: disable=W0612 + for cert_num, ((ip_address, port), + cert) in enumerate(certificates.items()): + pages[cert_num] = {} + + # Extract certificate data + not_valid_before = cert.not_valid_before + not_valid_after = cert.not_valid_after + version_value = f'{cert.version.value + 1} ({hex(cert.version.value)})' + signature_alg_value = cert.signature_algorithm_oid._name + not_before = str(not_valid_before) + not_after = str(not_valid_after) + public_key = cert.public_key() + signed_by = 'None' + + if isinstance(public_key, rsa.RSAPublicKey): + public_key_type = 'RSA' + elif isinstance(public_key, dsa.DSAPublicKey): + public_key_type = 'DSA' + elif isinstance(public_key, ec.EllipticCurvePublicKey): + public_key_type = 'EC' + else: + public_key_type = 'Unknown' + + # Calculate certificate length + cert_length = len( + cert.public_bytes(encoding=serialization.Encoding.DER)) + + # Append certification information + pages[cert_num]['cert_info_data'] = { + 'Version': version_value, + 'Signature Alg.': signature_alg_value, + 'Validity from': not_before, + 'Valid to': not_after, + } + + # Append the subject information + pages[cert_num]['subject_data'] = {} + for val in cert.subject.rdns: + dn = val.rfc4514_string().split('=') + pages[cert_num]['subject_data'][dn[0]] = dn[1] + + # Append issuer information + for val in cert.issuer.rdns: + dn = val.rfc4514_string().split('=') + if 'CN' in dn[0]: + signed_by = dn[1] + + # Append extensions information + if cert.extensions: + pages[cert_num]['cert_ext'] = {} + for extension in cert.extensions: + if isinstance(extension.value, list): + for extension_value in extension.value: + extension_name = extension.oid._name + formatted_value = self.format_extension_value( + extension_value.value) + pages[cert_num]['cert_ext'][extension_name] = formatted_value + else: + formatted_value = self.format_extension_value( + extension.value) + pages[cert_num]['cert_ext'][extension.oid._name] = formatted_value + + pages[cert_num]['summary_data'] = [ + not_after, + cert_length, + public_key_type, + port, + signed_by + ] + + outbound_conns = self._tls_util.get_all_outbound_connections( + device_mac=self._device_mac, capture_files=pcap_files) + + report_jinja = '' + if pages: + for num,page in pages.items(): + module_header_repr = module_header if num == 0 else None + cert_ext=page['cert_ext'] if 'cert_ext' in page else None + page_html = template.render( + base_template=self._base_template_file, + module_header=module_header_repr, + summary_headers=summary_headers, + summary_data=page['summary_data'], + cert_info_data=page['cert_info_data'], + subject_data=page['subject_data'], + cert_table_headers=cert_table_headers, + cert_ext=cert_ext, + ountbound_headers=outbound_headers, + ) + report_jinja += page_html + if outbound_conns: + out_page = template.render( + base_template=self._base_template_file, + ountbound_headers=outbound_headers, + outbound_conns=outbound_conns + ) + report_jinja += out_page + else: + report_jinja = template.render( + base_template=self._base_template_file, + module_header = module_header, + ) + LOGGER.debug('Module report:\n' + report_jinja) + + # Use os.path.join to create the complete file path + jinja_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME) + + # Write the content to a file + with open(jinja_path, 'w', encoding='utf-8') as file: + file.write(report_jinja) + + LOGGER.info('Module report generated at: ' + str(jinja_path)) + return jinja_path + + def format_extension_value(self, value): + if isinstance(value, bytes): + # Convert byte sequences to hex strings + return hexlify(value).decode() + elif isinstance(value, (list, tuple)): + # Format lists/tuples for HTML output + return ', '.join([self.format_extension_value(v) for v in value]) + elif isinstance(value, ExtendedKeyUsage): + # Handle ExtendedKeyUsage extension + return ', '.join( + [oid._name or f'Unknown OID ({oid.dotted_string})' for oid in value]) + elif isinstance(value, GeneralNames): + # Handle GeneralNames (used in SubjectAlternativeName) + return ', '.join( + [name.value for name in value if isinstance(name, DNSName)]) + elif isinstance(value, SubjectAlternativeName): + # Extract and format the GeneralNames (which contains DNSName, + #IPAddress, etc.) + return self.format_extension_value(value.get_values_for_type(DNSName)) + + elif isinstance(value, ObjectIdentifier): + # Handle ObjectIdentifier directly + return value._name or f'Unknown OID ({value.dotted_string})' + elif hasattr(value, '_name'): + # Extract the name for OIDs (Object Identifiers) + return value._name + elif isinstance(value, AuthorityKeyIdentifier): + # Handle AuthorityKeyIdentifier extension + key_id = self.format_extension_value(value.key_identifier) + cert_issuer = value.authority_cert_issuer + cert_serial = value.authority_cert_serial_number + + return (f'key_identifier={key_id}, ' + f'authority_cert_issuer={cert_issuer}, ' + f'authority_cert_serial_number={cert_serial}') + elif isinstance(value, SubjectKeyIdentifier): + # Handle SubjectKeyIdentifier extension + return f'digest={self.format_extension_value(value.digest)}' + elif isinstance(value, BasicConstraints): + # Handle BasicConstraints extension + return f'ca={value.ca}, path_length={value.path_length}' + elif isinstance(value, KeyUsage): + # Handle KeyUsage extension + return (f'digital_signature={value.digital_signature}, ' + f'key_cert_sign={value.key_cert_sign}, ' + f'key_encipherment={value.key_encipherment}, ' + f'crl_sign={value.crl_sign}') + return str(value) # Fallback to string conversion + + def generate_outbound_connection_table(self, outbound_conns): + """Generate just an HTML table from a list of IPs""" + html_content = ''' +

Outbound Connections

+ + + + + + + + + ''' + + rows = [ + f'\t' + for ip, port in outbound_conns + ] + html_content += '\n'.join(rows) + + # Close the table + html_content += """ + + \r
Destination IPPort
{ip}{port}
+ """ + + return html_content def extract_certificates_from_pcap(self, pcap_files, mac_address): # Initialize a list to store packets all_packets = [] # Iterate over each file for pcap_file in pcap_files: - # Open the capture file - packets = pyshark.FileCapture(pcap_file) + # Open the capture file and filter by tls + packets = pyshark.FileCapture(pcap_file, display_filter='tls') try: # Iterate over each packet in the file and add it to the list for packet in packets: @@ -223,119 +319,187 @@ def extract_certificates_from_pcap(self, pcap_files, mac_address): port = packet.tcp.srcport if 'tcp' in packet else packet.udp.srcport # Store certificate in dictionary with IP address and port as key certificates[(ip_address, port)] = certificate - return certificates + sorted_keys = sorted(certificates.keys(), key=lambda x: (x[0], x[1])) + sorted_certificates = {k: certificates[k] for k in sorted_keys} + return sorted_certificates def _security_tls_v1_2_server(self): LOGGER.info('Running security.tls.v1_2_server') - self._resolve_device_ip() # If the ipv4 address wasn't resolved yet, try again + self._resolve_device_ip() + ports_valid = [] + ports_invalid = [] + result = None + details = '' + description = '' if self._device_ipv4_addr is not None: - tls_1_2_results = self._tls_util.validate_tls_server( - self._device_ipv4_addr, tls_version='1.2') - tls_1_3_results = self._tls_util.validate_tls_server( - self._device_ipv4_addr, tls_version='1.3') - results = self._tls_util.process_tls_server_results(tls_1_2_results, - tls_1_3_results) + if self._scan_results is None: + self._scan_results = self._http_scan.scan_for_http_services( + self._device_ipv4_addr) + if self._scan_results is not None: + for port, service_type in self._scan_results.items(): + if 'HTTPS' in service_type: + LOGGER.info(f'Inspecting Service on port {port}: {service_type}') + tls_1_2_results = self._tls_util.validate_tls_server( + host=self._device_ipv4_addr, port=port, tls_version='1.2') + tls_1_3_results = self._tls_util.validate_tls_server( + host=self._device_ipv4_addr, port=port, tls_version='1.3') + port_results = self._tls_util.process_tls_server_results( + tls_1_2_results, tls_1_3_results, port=port) + if port_results is not None: + result = port_results[ + 0] if result is None else result and port_results[0] + details += port_results[1] + if port_results[0]: + ports_valid.append(port) + else: + ports_invalid.append(port) + elif 'HTTP' in service_type: + # Any non-HTTPS service detetcted is automatically invalid + ports_invalid.append(port) + details += f'\nHTTP service detected on port {port}' + result = False + LOGGER.debug(f'Valid Ports: {ports_valid}') + LOGGER.debug(f'Invalid Ports: {ports_invalid}') # Determine results and return proper messaging and details - description = '' - if results[0] is None: + if result is None: + result = 'Feature Not Detected' description = 'TLS 1.2 certificate could not be validated' - elif results[0]: - description = 'TLS 1.2 certificate is valid' + elif result: + ports_csv = ','.join(map(str,ports_valid)) + description = f'TLS 1.2 certificate valid on ports: {ports_csv}' else: - description = 'TLS 1.2 certificate is invalid' - return results[0], description,results[1] - + ports_csv = ','.join(map(str,ports_invalid)) + description = f'TLS 1.2 certificate invalid on ports: {ports_csv}' + return result, description, details else: LOGGER.error('Could not resolve device IP address. Skipping') return 'Error', 'Could not resolve device IP address' def _security_tls_v1_3_server(self): LOGGER.info('Running security.tls.v1_3_server') - self._resolve_device_ip() # If the ipv4 address wasn't resolved yet, try again + self._resolve_device_ip() + ports_valid = [] + ports_invalid = [] + result = None + details = '' + description = '' if self._device_ipv4_addr is not None: - results = self._tls_util.validate_tls_server(self._device_ipv4_addr, - tls_version='1.3') + if self._scan_results is None: + self._scan_results = self._http_scan.scan_for_http_services( + self._device_ipv4_addr) + if self._scan_results is not None: + for port, service_type in self._scan_results.items(): + if 'HTTPS' in service_type: + LOGGER.info(f'Inspecting Service on port {port}: {service_type}') + port_results = self._tls_util.validate_tls_server( + self._device_ipv4_addr, tls_version='1.3', port=port) + if port_results is not None: + result = port_results[ + 0] if result is None else result and port_results[0] + details += port_results[1] + if port_results[0]: + ports_valid.append(port) + else: + ports_invalid.append(port) + elif 'HTTP' in service_type: + # Any non-HTTPS service detetcted is automatically invalid + ports_invalid.append(port) + details += f'\nHTTP service detected on port {port}' + result = False + LOGGER.debug(f'Valid Ports: {ports_valid}') + LOGGER.debug(f'Invalid Ports: {ports_invalid}') # Determine results and return proper messaging and details - description = '' - if results[0] is None: + if result is None: + result = 'Feature Not Detected' description = 'TLS 1.3 certificate could not be validated' - elif results[0]: - description = 'TLS 1.3 certificate is valid' + elif result: + ports_csv = ','.join(map(str,ports_valid)) + description = f'TLS 1.3 certificate valid on ports: {ports_csv}' else: - description = 'TLS 1.3 certificate is invalid' - return results[0], description,results[1] - + ports_csv = ','.join(map(str,ports_invalid)) + description = f'TLS 1.3 certificate invalid on ports: {ports_csv}' + return result, description, details else: LOGGER.error('Could not resolve device IP address') return 'Error', 'Could not resolve device IP address' + def _security_tls_v1_0_client(self): + LOGGER.info('Running security.tls.v1_0_client') + tls_1_0_valid = self._validate_tls_client(self._device_mac, '1.0') + tls_1_1_valid = self._validate_tls_client(self._device_mac, '1.1') + tls_1_2_valid = self._validate_tls_client(self._device_mac, '1.2') + tls_1_3_valid = self._validate_tls_client(self._device_mac, '1.3') + states = [ + tls_1_0_valid[0], tls_1_1_valid[0], tls_1_2_valid[0], tls_1_3_valid[0] + ] + if any(state is True for state in states): + # If any state is True, return True + result_state = True + result_message = 'TLS 1.0 or higher detected' + elif all(state == 'Feature Not Detected' for state in states): + # If all states are "Feature not Detected" + result_state = 'Feature Not Detected' + result_message = tls_1_0_valid[1] + elif all(state == 'Error' for state in states): + # If all states are "Error" + result_state = 'Error' + result_message = '' + else: + result_state = False + result_message = 'TLS 1.0 or higher was not detected' + result_details = tls_1_0_valid[2] + tls_1_1_valid[2] + tls_1_2_valid[ + 2] + tls_1_3_valid[2] + result_tags = list( + set(tls_1_0_valid[3] + tls_1_1_valid[3] + tls_1_2_valid[3] + + tls_1_3_valid[3])) + return result_state, result_message, result_details, result_tags + def _security_tls_v1_2_client(self): LOGGER.info('Running security.tls.v1_2_client') - self._resolve_device_ip() - # If the ipv4 address wasn't resolved yet, try again - if self._device_ipv4_addr is not None: - results = self._validate_tls_client(self._device_ipv4_addr, '1.2') - # Determine results and return proper messaging and details - description = '' - result = None - if results[0] is None: - description = 'No outbound connections were found' - result = 'Feature Not Detected' - elif results[0]: - description = 'TLS 1.2 client connections valid' - result = True - else: - description = 'TLS 1.2 client connections invalid' - result = False - return result, description, results[1] - else: - LOGGER.error('Could not resolve device IP address. Skipping') - return 'Error', 'Could not resolve device IP address' + return self._validate_tls_client(self._device_mac, + '1.2', + unsupported_versions=['1.0', '1.1']) def _security_tls_v1_3_client(self): LOGGER.info('Running security.tls.v1_3_client') - self._resolve_device_ip() - # If the ipv4 address wasn't resolved yet, try again - if self._device_ipv4_addr is not None: - results = self._validate_tls_client(self._device_ipv4_addr, '1.3') - # Determine results and return proper messaging and details - description = '' - if results[0] is None: - description = 'No outbound connections were found' - elif results[0]: - description = 'TLS 1.3 client connections valid' - else: - description = 'TLS 1.3 client connections invalid' - return results[0], description, results[1] - else: - LOGGER.error('Could not resolve device IP address') - return 'Error', 'Could not resolve device IP address' - - def _validate_tls_client(self, client_ip, tls_version): + return self._validate_tls_client(self._device_mac, + '1.3', + unsupported_versions=['1.0', '1.1']) + + def _validate_tls_client(self, + client_mac, + tls_version, + unsupported_versions=None): client_results = self._tls_util.validate_tls_client( - client_ip=client_ip, + client_mac=client_mac, tls_version=tls_version, capture_files=[ MONITOR_CAPTURE_FILE, STARTUP_CAPTURE_FILE, TLS_CAPTURE_FILE - ]) + ], + unsupported_versions=unsupported_versions) # Generate results based on the state - result_message = 'No outbound connections were found.' - result_state = 'Feature Not Detected' + result_state = None + result_message = '' + result_details = '' + result_tags = [] - # If any of the packets detect failed client comms, fail the test - if not client_results[0] and client_results[0] is not None: - result_state = False - result_message = client_results[1] - else: + if client_results[0] is not None: + result_details = client_results[1] if client_results[0]: result_state = True - result_message = client_results[1] - return result_state, result_message + result_message = f'TLS {tls_version} client connections valid' + else: + result_state = False + result_message = f'TLS {tls_version} client connections invalid' + else: + result_state = 'Feature Not Detected' + result_message = 'No outbound TLS connections were found' + return result_state, result_message, result_details, result_tags def _resolve_device_ip(self): # If the ipv4 address wasn't resolved yet, try again - if self._device_ipv4_addr is None: + if self._device_ipv4_addr is None: # pylint: disable=E0203 self._device_ipv4_addr = self._get_device_ipv4() diff --git a/modules/test/tls/python/src/tls_util.py b/modules/test/tls/python/src/tls_util.py index d8c1d7a16..a60332e2d 100644 --- a/modules/test/tls/python/src/tls_util.py +++ b/modules/test/tls/python/src/tls_util.py @@ -25,6 +25,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from ipaddress import IPv4Address +from scapy.all import rdpcap, IP, Ether, TCP, UDP LOG_NAME = 'tls_util' LOGGER = None @@ -37,6 +38,7 @@ ipaddress.ip_network('172.16.0.0/12'), ipaddress.ip_network('192.168.0.0/16') ] +TR_CONTAINER_MAC_PREFIX = '9a:02:57:1e:8f:' #Define the allowed protocols as tshark filters DEFAULT_ALLOWED_PROTOCOLS = ['quic'] @@ -59,6 +61,59 @@ def __init__(self, if allowed_protocols is None: self._allowed_protocols = DEFAULT_ALLOWED_PROTOCOLS + def get_all_outbound_connections(self, device_mac, capture_files): + """Process multiple pcap files and combine unique IP destinations in the + order of first appearance.""" + + all_outbound_conns = [] + for capture in capture_files: + ips = self.get_outbound_connections(device_mac=device_mac, + capture_file=capture) + all_outbound_conns.extend(ips) # Collect all connections sequentially + + # Remove duplicates while preserving the first-seen order + unique_ordered_conns = list(dict.fromkeys(all_outbound_conns)) + return unique_ordered_conns + + def get_outbound_connections(self, device_mac, capture_file): + """Extract unique IP and port destinations from a single pcap file + based on the known MAC address, preserving the order of appearance.""" + packets = rdpcap(capture_file) + outbound_conns = [] + + for packet in packets: + if Ether in packet and IP in packet: + if packet[Ether].src == device_mac: + ip_dst = packet[IP].dst + port_dst = 'Unknown' + + # Check if the packet has TCP or UDP layer to get the destination port + if TCP in packet: + port_dst = packet[TCP].dport + elif UDP in packet: + port_dst = packet[UDP].dport + + if self.is_external_ip(ip_dst): + # Add to list as a tuple + outbound_conns.append((ip_dst, port_dst)) + + # Use dict.fromkeys to remove duplicates while preserving insertion order + unique_conns = list(dict.fromkeys(outbound_conns)) + return unique_conns + + def is_external_ip(self, ip): + """Check if the IP is an external (non-private) IP address.""" + try: + # Convert the IP string into an IPv4Address object + ip_addr = ipaddress.ip_address(ip) + + # Return True only if the IP is not in a private or reserved range + return not (ip_addr.is_private or ip_addr.is_loopback + or ip_addr.is_link_local) + except ValueError: + # Return False if the IP is invalid or not IPv4 + return False + def get_public_certificate(self, host, port=443, @@ -103,6 +158,9 @@ def get_public_certificate(self, except socket.timeout: LOGGER.info('Socket timeout error') return None + except OSError as e: + LOGGER.error(e) + return None return cert_pem @@ -166,11 +224,11 @@ def verify_public_key(self, public_key): else: return False, 'Key is not RSA or EC type' - def validate_signature(self, host): + def validate_signature(self, host, port): # Reconnect to the device but with validate signature option # set to true which will check for proper cert chains # within the valid CA root certs stored on the server - if self.validate_trusted_ca_signature(host): + if self.validate_trusted_ca_signature(host, port): LOGGER.info('Authorized Certificate Authority signature confirmed') return True, 'Authorized Certificate Authority signature confirmed' else: @@ -206,13 +264,14 @@ def validate_local_ca_signature(self, device_cert_path): LOGGER.error(str(e)) return False, None - def validate_trusted_ca_signature(self, host): + def validate_trusted_ca_signature(self, host, port): # Reconnect to the device but with validate signature option # set to true which will check for proper cert chains # within the valid CA root certs stored on the server LOGGER.info( 'Checking for valid signature from authorized Certificate Authorities') - public_cert = self.get_public_certificate(host, + public_cert = self.get_public_certificate(host=host, + port=port, validate_cert=True, tls_version='1.2') if public_cert: @@ -236,8 +295,8 @@ def validate_cert_chain(self, device_cert_path): ca_issuer_cert, cert_file_path = self.get_ca_issuer(certificate) if ca_issuer_cert is not None and cert_file_path is not None: LOGGER.info('CA Issuer resolved') - cert_text = crypto.dump_certificate(crypto.FILETYPE_TEXT, - ca_issuer_cert).decode() + cert_text = ca_issuer_cert.public_bytes( + encoding=serialization.Encoding.PEM).decode() LOGGER.info(cert_text) return self.validate_trusted_ca_signature_chain( device_cert_path=device_cert_path, @@ -322,34 +381,41 @@ def get_certificate(self, uri, timeout=10): LOGGER.error(f'Error fetching certificate from URI: {e}') return certificate - def process_tls_server_results(self, tls_1_2_results, tls_1_3_results): + def process_tls_server_results(self, tls_1_2_results, tls_1_3_results, port): results = '' if tls_1_2_results[0] is None and tls_1_3_results[0] is not None: # Validate only TLS 1.3 results - description = 'TLS 1.3' + (' not' if not tls_1_3_results[0] else - '') + ' validated: ' + tls_1_3_results[1] + description = (f"""TLS 1.3 {'' if tls_1_3_results[0] else 'not '}""" + f"""validated on port {port}: """ + f"""{tls_1_3_results[1]}""") results = tls_1_3_results[0], description elif tls_1_3_results[0] is None and tls_1_2_results[0] is not None: # Vaidate only TLS 1.2 results - description = 'TLS 1.2' + (' not' if not tls_1_2_results[0] else - '') + ' validated: ' + tls_1_2_results[1] + description = (f"""TLS 1.2 {'' if tls_1_2_results[0] else 'not '}""" + f"""validated on port {port}: """ + f"""{tls_1_2_results[1]}""") results = tls_1_2_results[0], description elif tls_1_3_results[0] is not None and tls_1_2_results[0] is not None: # Validate both results - description = 'TLS 1.2' + (' not' if not tls_1_2_results[0] else - '') + ' validated: ' + tls_1_2_results[1] - description += '\nTLS 1.3' + (' not' if not tls_1_3_results[0] else - '') + ' validated: ' + tls_1_3_results[1] + description = (f"""TLS 1.2 {'' if tls_1_2_results[0] else 'not '}""" + f"""validated on port {port}: """ + f"""{tls_1_2_results[1]}""") + description += '\n'+(f"""TLS 1.3 {'' if tls_1_3_results[0] else 'not '}""" + f"""validated on port {port}: """ + f"""{tls_1_3_results[1]}""") results = tls_1_2_results[0] or tls_1_3_results[0], description else: - description = f'TLS 1.2 not validated: {tls_1_2_results[1]}' - description += f'\nTLS 1.3 not validated: {tls_1_3_results[1]}' + description = (f"""TLS 1.2 not validated on port {port}: """ + f"""{tls_1_2_results[1]}""") + description += '\n'+(f"""TLS 1.3 not validated on port {port}: """ + f"""{tls_1_3_results[1]}""") results = None, description LOGGER.info('TLS server test results: ' + str(results)) return results - def validate_tls_server(self, host, tls_version): - cert_pem = self.get_public_certificate(host, + def validate_tls_server(self, host, tls_version, port=443): + cert_pem = self.get_public_certificate(host=host, + port=port, validate_cert=False, tls_version=tls_version) if cert_pem: @@ -372,14 +438,15 @@ def validate_tls_server(self, host, tls_version): public_key = self.get_public_key(public_cert) if public_key: key_valid = self.verify_public_key(public_key) + else: + key_valid = [0] - sig_valid = self.validate_signature(host) + sig_valid = self.validate_signature(host=host, port=port) # Check results cert_valid = tr_valid[0] and key_valid[0] and sig_valid[0] test_details = tr_valid[1] + '\n' + key_valid[1] + '\n' + sig_valid[1] LOGGER.info('Certificate validated: ' + str(cert_valid)) - LOGGER.info('Test details:\n' + test_details) return cert_valid, test_details else: LOGGER.info('Failed to resolve public certificate') @@ -418,11 +485,11 @@ def get_ciphers(self, capture_file, dst_ip, dst_port): ciphers = response[0].split('\n') return ciphers - def get_hello_packets(self, capture_files, src_ip, tls_version): + def get_hello_packets(self, capture_files, src_mac, tls_version): combined_results = [] for capture_file in capture_files: bin_file = self._bin_dir + '/get_client_hello_packets.sh' - args = f'"{capture_file}" {src_ip} {tls_version}' + args = f'"{capture_file}" {src_mac} {tls_version}' command = f'{bin_file} {args}' response = util.run_command(command) packets = response[0].strip() @@ -444,11 +511,11 @@ def get_handshake_complete(self, capture_files, src_ip, dst_ip, tls_version): return combined_results # Resolve all connections from the device that don't use TLS - def get_non_tls_packetes(self, client_ip, capture_files): + def get_non_tls_packetes(self, client_mac, capture_files): combined_packets = [] for capture_file in capture_files: bin_file = self._bin_dir + '/get_non_tls_client_connections.sh' - args = f'"{capture_file}" {client_ip}' + args = f'"{capture_file}" {client_mac}' command = f'{bin_file} {args}' response = util.run_command(command) if len(response) > 0: @@ -458,13 +525,13 @@ def get_non_tls_packetes(self, client_ip, capture_files): # Resolve all connections from the device that use TLS def get_tls_client_connection_packetes(self, - client_ip, + client_mac, capture_files, protocol=None): combined_packets = [] for capture_file in capture_files: bin_file = self._bin_dir + '/get_tls_client_connections.sh' - args = f'"{capture_file}" {client_ip}' + args = f'"{capture_file}" {client_mac}' if protocol is not None: args += f' {protocol}' command = f'{bin_file} {args}' @@ -477,16 +544,17 @@ def get_tls_client_connection_packetes(self, # connections are established or any other validation only # that there is some level of connection attempt from the device # using the TLS version specified. - def get_tls_packets(self, capture_files, src_ip, tls_version): + def get_tls_packets(self, capture_files, src_mac, tls_version): combined_results = [] for capture_file in capture_files: bin_file = self._bin_dir + '/get_tls_packets.sh' - args = f'"{capture_file}" {src_ip} {tls_version}' + args = f'"{capture_file}" {src_mac} {tls_version}' command = f'{bin_file} {args}' response = util.run_command(command) packets = response[0].strip() + parsed_json = json.loads(packets) # Parse each packet and append key-value pairs to combined_results - result = self.parse_packets(json.loads(packets), capture_file) + result = self.parse_packets(parsed_json, capture_file) combined_results.extend(result) return combined_results @@ -527,7 +595,7 @@ def process_hello_packets(self, LOGGER.info('Checking client ciphers: ' + str(packet)) if packet['cipher_support']['ecdh'] and packet['cipher_support'][ 'ecdsa']: - LOGGER.info('Valid ciphers detected') + LOGGER.info('Required ciphers detected') client_hello_results['valid'].append(packet) # If a previous hello packet to the same destination failed, # we can now remove it as it has passed on a different attempt @@ -537,7 +605,7 @@ def process_hello_packets(self, if packet['dst_ip'] in str(invalid_packet): client_hello_results['invalid'].remove(invalid_packet) else: - LOGGER.info('Invalid ciphers detected') + LOGGER.info('Required ciphers not detected') if packet['dst_ip'] not in allowed_protocol_client_ips: if packet['dst_ip'] not in str(client_hello_results['invalid']): client_hello_results['invalid'].append(packet) @@ -549,7 +617,7 @@ def process_hello_packets(self, f'\nAllowing {protocol_name} traffic to {packet["dst_ip"]}') client_hello_results['valid'].append(packet) else: - # No cipher check for TLS 1.3 + # No cipher check for TLS 1.0, 1.1 or TLS 1.3 client_hello_results['valid'] = hello_packets return client_hello_results @@ -558,16 +626,12 @@ def process_hello_packets(self, # we will assume any local connections using the same IP subnet as our # local network are approved and only connections to IP addresses outside # our network will be flagged. - def get_non_tls_client_connection_ips(self, client_ip, capture_files): + def get_non_tls_client_connection_ips(self, client_mac, capture_files): LOGGER.info('Checking client for non-TLS client connections') - packets = self.get_non_tls_packetes(client_ip=client_ip, + packets = self.get_non_tls_packetes(client_mac=client_mac, capture_files=capture_files) # Extract the subnet from the client IP address - src_ip = ipaddress.ip_address(client_ip) - src_subnet = ipaddress.ip_network(src_ip, strict=False) - subnet_with_mask = ipaddress.ip_network( - src_subnet, strict=False).supernet(new_prefix=24) non_tls_dst_ips = set() # Store unique destination IPs for packet in packets: @@ -576,6 +640,13 @@ def get_non_tls_client_connection_ips(self, client_ip, capture_files): tcp_flags = packet['_source']['layers']['tcp.flags'] if 'A' not in tcp_flags and 'S' not in tcp_flags: # Packet is not ACK or SYN + + src_ip = ipaddress.ip_address( + packet['_source']['layers']['ip.src'][0]) + src_subnet = ipaddress.ip_network(src_ip, strict=False) + subnet_with_mask = ipaddress.ip_network( + src_subnet, strict=False).supernet(new_prefix=24) + dst_ip = ipaddress.ip_address( packet['_source']['layers']['ip.dst'][0]) if not dst_ip in subnet_with_mask: @@ -587,44 +658,39 @@ def get_non_tls_client_connection_ips(self, client_ip, capture_files): # we will assume any local connections using the same IP subnet as our # local network are approved and only connections to IP addresses outside # our network will be flagged. - def get_unsupported_tls_ips(self, client_ip, capture_files): + def get_unsupported_tls_ips(self, + client_mac, + capture_files, + unsupported_versions=None): LOGGER.info('Checking client for unsupported TLS client connections') - tls_1_0_packets = self.get_tls_packets(capture_files, client_ip, '1.0') - tls_1_1_packets = self.get_tls_packets(capture_files, client_ip, '1.1') - unsupported_tls_dst_ips = {} - if len(tls_1_0_packets) > 0: - for packet in tls_1_0_packets: - dst_ip = packet['dst_ip'] - tls_version = '1.0' - if dst_ip not in unsupported_tls_dst_ips: - LOGGER.info(f'''Unsupported TLS {tls_version} - connections detected to {dst_ip}''') - unsupported_tls_dst_ips[dst_ip] = [tls_version] - - if len(tls_1_1_packets) > 0: - for packet in tls_1_1_packets: - dst_ip = packet['dst_ip'] - tls_version = '1.1' - # Check if the IP is already in the dictionary - if dst_ip in unsupported_tls_dst_ips: - # If the IP is already present, append the new TLS version to the - # list - unsupported_tls_dst_ips[dst_ip].append(tls_version) - else: - # If the IP is not present, create a new list with the current - # TLS version - LOGGER.info(f'''Unsupported TLS {tls_version} connections detected - to {dst_ip}''') - unsupported_tls_dst_ips[dst_ip] = [tls_version] + if unsupported_versions is not None: + for unsupported_version in unsupported_versions: + tls_packets = self.get_tls_packets(capture_files, client_mac, '1.0') + if len(tls_packets) > 0: + for packet in tls_packets: + dst_ip = packet['dst_ip'] + tls_version = unsupported_version + if dst_ip not in unsupported_tls_dst_ips: + # If the IP is already present, append the new TLS version to the + # list + LOGGER.info(f'''Unsupported TLS {tls_version} + connections detected to {dst_ip}''') + unsupported_tls_dst_ips[dst_ip] = [tls_version] + else: + # If the IP is not present, create a new list with the current + # TLS version + LOGGER.info(f'''Unsupported TLS {tls_version} connections detected + to {dst_ip}''') + unsupported_tls_dst_ips[dst_ip] = [tls_version] return unsupported_tls_dst_ips # Check if the device has made any outbound connections that use any # version of TLS. - def get_tls_client_connection_ips(self, client_ip, capture_files): + def get_tls_client_connection_ips(self, client_mac, capture_files): LOGGER.info('Checking client for TLS client connections') packets = self.get_tls_client_connection_packetes( - client_ip=client_ip, capture_files=capture_files) + client_mac=client_mac, capture_files=capture_files) tls_dst_ips = set() # Store unique destination IPs for packet in packets: @@ -634,13 +700,13 @@ def get_tls_client_connection_ips(self, client_ip, capture_files): # Check if the device has made any outbound connections that use any # allowed protocols that do not fit into a direct TLS packet inspection - def get_allowed_protocol_client_connection_ips(self, client_ip, + def get_allowed_protocol_client_connection_ips(self, client_mac, capture_files): LOGGER.info('Checking client for TLS Protocol client connections') tls_dst_ips = {} # Store unique destination IPs with the protocol name for protocol in self._allowed_protocols: packets = self.get_tls_client_connection_packetes( - client_ip=client_ip, capture_files=capture_files, protocol=protocol) + client_mac=client_mac, capture_files=capture_files, protocol=protocol) for packet in packets: dst_ip = ipaddress.ip_address(packet['_source']['layers']['ip.dst'][0]) @@ -655,20 +721,26 @@ def is_private_ip(self, ip): return True return False - def validate_tls_client(self, client_ip, tls_version, capture_files): + def validate_tls_client(self, + client_mac, + tls_version, + capture_files, + unsupported_versions=None): LOGGER.info('Validating client for TLS: ' + tls_version) - hello_packets = self.get_hello_packets(capture_files, client_ip, + hello_packets = self.get_hello_packets(capture_files, client_mac, tls_version) # Resolve allowed protocol connections that require # additional consideration beyond packet inspection - allowed_protocol_client_ips = ( - self.get_allowed_protocol_client_connection_ips(client_ip, - capture_files)) + protocol_client_ips = (self.get_allowed_protocol_client_connection_ips( + client_mac, capture_files)) - LOGGER.info(f'Protocol IPS: {allowed_protocol_client_ips}') - client_hello_results = self.process_hello_packets( - hello_packets, allowed_protocol_client_ips, tls_version) + if len(protocol_client_ips) > 0: + LOGGER.info( + f'Allowed Protocol IP connections detected: {protocol_client_ips}') + client_hello_results = self.process_hello_packets(hello_packets, + protocol_client_ips, + tls_version) handshakes = {'complete': [], 'incomplete': []} for packet in client_hello_results['valid']: @@ -732,10 +804,10 @@ def validate_tls_client(self, client_ip, tls_version, capture_files): # Resolve all non-TLS related client connections non_tls_client_ips = self.get_non_tls_client_connection_ips( - client_ip, capture_files) + client_mac, capture_files) # Resolve all TLS related client connections - tls_client_ips = self.get_tls_client_connection_ips(client_ip, + tls_client_ips = self.get_tls_client_connection_ips(client_mac, capture_files) # Filter out all outbound TLS connections regardless on whether # or not they were validated. If they were not validated, @@ -756,7 +828,9 @@ def validate_tls_client(self, client_ip, tls_version, capture_files): LOGGER.info(f'''TLS connection detected to {ip}. Ignoring non-TLS traffic detected to this IP''') - unsupported_tls_ips = self.get_unsupported_tls_ips(client_ip, capture_files) + unsupported_tls_ips = self.get_unsupported_tls_ips(client_mac, + capture_files, + unsupported_versions) if len(unsupported_tls_ips) > 0: tls_client_valid = False for ip, tls_versions in unsupported_tls_ips.items(): diff --git a/modules/test/tls/resources/report_template.jinja2 b/modules/test/tls/resources/report_template.jinja2 new file mode 100644 index 000000000..5680d9339 --- /dev/null +++ b/modules/test/tls/resources/report_template.jinja2 @@ -0,0 +1,90 @@ +{% extends base_template %} +{% block content %} +{% if cert_info_data and subject_data %} +
+
+
Certificate Information
+ + + + {% for header in cert_table_headers%} + + {% endfor %} + + + + {% for k,v in cert_info_data.items() %} + + + + + {% endfor %} + +
{{ header }}
{{ k }}{{ v }}
+
+
+
Subject Information
+ + + + {% for header in cert_table_headers%} + + {% endfor %} + + + + {% for k,v in subject_data.items() %} + + + + + {% endfor %} + +
{{ header }}
{{ k }}{{ v }}
+
+
+ {% if cert_ext %} +
Certificate Extensions
+ + + + + + + + + {% for k,v in cert_ext.items()%} + + + + + {% endfor %} + +
PropertyValue
{{ k }}{{ v }}
+ {% endif %} +{% elif ountbound_headers %} +

Outbound Connections

+ + + + {% for header in ountbound_headers%} + + {% endfor %} + + + + {% for ip, port in outbound_conns%} + + + + + {% endfor %} + +
{{ header }}
{{ip}}{{port}}
+{% else %} +
+
+ No TLS certificates found on the device +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/modules/test/tls/tls.Dockerfile b/modules/test/tls/tls.Dockerfile index cedf9531b..3d6d66544 100644 --- a/modules/test/tls/tls.Dockerfile +++ b/modules/test/tls/tls.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/tls-test -FROM test-run/base-test:latest +# Image name: testrun/tls-test +FROM testrun/base-test:latest # Set DEBIAN_FRONTEND to noninteractive mode ENV DEBIAN_FRONTEND=noninteractive @@ -31,14 +31,26 @@ COPY $MODULE_DIR/conf /testrun/conf # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin +# Remove incorrect line endings +RUN dos2unix /testrun/bin/* + +# Make sure all the bin files are executable +RUN chmod u+x /testrun/bin/* + # Copy over all python files COPY $MODULE_DIR/python /testrun/python -#Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +# Install all python requirements for the module +RUN pip install -r /testrun/python/requirements.txt -# Create a directory inside the container to store the root certificates -RUN mkdir -p /testrun/root_certs +# Install all python requirements for the modules unit test +RUN pip install -r /testrun/python/requirements-test.txt +# Install all python requirements for the modules unit test +RUN pip3 install -r /testrun/python/requirements-test.txt +# Create a directory inside the container to store the root certificates +RUN mkdir -p /testrun/root_certs +# Copy Jinja template +COPY $MODULE_DIR/resources/report_template.jinja2 $REPORT_TEMPLATE_PATH/ diff --git a/modules/ui/angular.json b/modules/ui/angular.json index d72fee51f..327fc2908 100644 --- a/modules/ui/angular.json +++ b/modules/ui/angular.json @@ -24,21 +24,40 @@ "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": ["src/favicon.ico", "src/assets"], - "styles": ["src/styles.scss"], - "scripts": [] + "styles": [ + "src/styles.scss", + "src/theming/m3-theme.scss", + "src/theming/colors.scss" + ], + "scripts": [], + "allowedCommonJsDependencies": ["mqtt-browser"], + "stylePreprocessorOptions": { + "includePaths": ["src/theming/"] + }, + "optimization": { + "scripts": true, + "styles": true, + "fonts": true + }, + "sourceMap": { + "scripts": true, + "styles": false, + "hidden": true, + "vendor": true + } }, "configurations": { "production": { "budgets": [ { "type": "initial", - "maximumWarning": "1000kb", - "maximumError": "3000kb" + "maximumWarning": "1500kb", + "maximumError": "4000kb" }, { "type": "anyComponentStyle", - "maximumWarning": "3kb", - "maximumError": "4kb" + "maximumWarning": "60kb", + "maximumError": "65kb" } ], "outputHashing": "all" @@ -56,12 +75,15 @@ }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "buildTarget": "test-run-ui:build" + }, "configurations": { "production": { - "browserTarget": "test-run-ui:build:production" + "buildTarget": "test-run-ui:build:production" }, "development": { - "browserTarget": "test-run-ui:build:development" + "buildTarget": "test-run-ui:build:development" } }, "defaultConfiguration": "development" @@ -69,7 +91,7 @@ "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "test-run-ui:build" + "buildTarget": "test-run-ui:build" } }, "test": { @@ -80,7 +102,11 @@ "inlineStyleLanguage": "scss", "assets": ["src/favicon.ico", "src/assets"], "styles": ["src/styles.scss"], - "scripts": [] + "scripts": [], + "karmaConfig": "karma.conf.js", + "stylePreprocessorOptions": { + "includePaths": ["src/theming/"] + } } }, "lint": { @@ -93,6 +119,7 @@ } }, "cli": { - "schematicCollections": ["@angular-eslint/schematics"] + "schematicCollections": ["@angular-eslint/schematics"], + "analytics": false } } diff --git a/modules/ui/build.Dockerfile b/modules/ui/build.Dockerfile new file mode 100644 index 000000000..082efbbcc --- /dev/null +++ b/modules/ui/build.Dockerfile @@ -0,0 +1,19 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Image name: testrun/build-ui +FROM node@sha256:ffebb4405810c92d267a764b21975fb2d96772e41877248a37bf3abaa0d3b590 as build + +# Set the working directory +WORKDIR /modules/ui \ No newline at end of file diff --git a/modules/ui/karma.conf.js b/modules/ui/karma.conf.js new file mode 100644 index 000000000..b6d22bd62 --- /dev/null +++ b/modules/ui/karma.conf.js @@ -0,0 +1,43 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma'), + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + }, + jasmineHtmlReporter: { + suppressAll: true, // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/test-run-ui'), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }], + check: { + global: { + statements: 75, + branches: 75, + functions: 75, + lines: 75, + }, + }, + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome'], + restartOnFileChange: true, + }); +}; diff --git a/modules/ui/package-lock.json b/modules/ui/package-lock.json index e6903631a..b023ab273 100644 --- a/modules/ui/package-lock.json +++ b/modules/ui/package-lock.json @@ -8,36 +8,39 @@ "name": "test-run-ui", "version": "0.0.0", "dependencies": { - "@angular/animations": "^17.0.8", - "@angular/cdk": "^17.0.4", - "@angular/common": "^17.0.8", - "@angular/compiler": "^17.0.8", - "@angular/core": "^17.0.8", - "@angular/forms": "^17.3.1", - "@angular/material": "^17.3.1", - "@angular/platform-browser": "^17.0.8", - "@angular/platform-browser-dynamic": "^17.3.1", - "@angular/router": "^17.3.1", - "@ngrx/component-store": "^17.1.1", - "@ngrx/effects": "^17.1.1", - "@ngrx/store": "^17.0.1", + "@angular/animations": "^19.0.1", + "@angular/cdk": "^19.0.1", + "@angular/common": "^19.0.1", + "@angular/compiler": "^19.0.1", + "@angular/core": "^19.0.1", + "@angular/forms": "^19.0.1", + "@angular/material": "^19.0.1", + "@angular/platform-browser": "^19.0.1", + "@angular/platform-browser-dynamic": "^19.0.1", + "@angular/router": "^19.0.1", + "@ngrx/component-store": "19.0.0-beta.0", + "@ngrx/effects": "19.0.0-beta.0", + "@ngrx/operators": "^19.0.0-beta.0", + "@ngrx/signals": "^19.0.0-beta.0", + "@ngrx/store": "19.0.0-beta.0", "ngx-mask": "^16.4.2", + "ngx-mqtt": "^17.0.0", "rxjs": "~7.8.0", "tslib": "^2.6.2", - "zone.js": "^0.14.4" + "zone.js": "^0.15.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.3.2", - "@angular-eslint/builder": "17.2.0", - "@angular-eslint/eslint-plugin": "17.2.0", - "@angular-eslint/eslint-plugin-template": "17.2.0", - "@angular-eslint/schematics": "17.2.0", - "@angular-eslint/template-parser": "17.2.0", - "@angular/cli": "~17.0.9", - "@angular/compiler-cli": "^17.3.1", + "@angular-devkit/build-angular": "^19.0.2", + "@angular-eslint/builder": "19.0.0-alpha.4", + "@angular-eslint/eslint-plugin": "19.0.0-alpha.4", + "@angular-eslint/eslint-plugin-template": "19.0.0-alpha.4", + "@angular-eslint/schematics": "19.0.0-alpha.4", + "@angular-eslint/template-parser": "19.0.0-alpha.4", + "@angular/cli": "~19.0.2", + "@angular/compiler-cli": "^19.0.1", "@types/jasmine": "~4.3.6", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", + "@typescript-eslint/eslint-plugin": "^8.2.0", + "@typescript-eslint/parser": "^8.2.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -48,8 +51,8 @@ "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "prettier": "^3.2.5", - "prettier-eslint": "^16.3.0", - "typescript": "~5.2.2" + "prettier-eslint": "^13.0.0", + "typescript": "~5.5.4" } }, "node_modules/@ampproject/remapping": { @@ -57,6 +60,7 @@ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -66,112 +70,117 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1703.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.6.tgz", - "integrity": "sha512-Ck501FD/QuOjeKVFs7hU92w8+Ffetv0d5Sq09XY2/uygo5c/thMzp9nkevaIWBxUSeU5RqYZizDrhFVgYzbbOw==", + "version": "0.1902.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.9.tgz", + "integrity": "sha512-SLUc7EaFMjhCnimqxTcv32wESJBLQ3E6c/1sAndPojyCoGiX24ASu2pxrTXrYNS9DqiJT8tReAnqmh7dmf3xwQ==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "17.3.6", + "@angular-devkit/core": "19.2.9", "rxjs": "7.8.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@angular-devkit/build-angular": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.6.tgz", - "integrity": "sha512-K4CEZvhQZUUOpmXPVoI1YBM8BARbIlqE6FZRxakmnr+YOtVTYE5s+Dr1wgja8hZIohNz6L7j167G9Aut7oPU/w==", + "version": "19.2.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.9.tgz", + "integrity": "sha512-v6x3h+LYyEew3EjoI1+2IiFDz6f96lJB1JvbbZj3Li9FMhO4M/xo4BaWHbeg9Lot/vUy6IAlR+BJywawNIzv0Q==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1703.6", - "@angular-devkit/build-webpack": "0.1703.6", - "@angular-devkit/core": "17.3.6", - "@babel/core": "7.24.0", - "@babel/generator": "7.23.6", - "@babel/helper-annotate-as-pure": "7.22.5", - "@babel/helper-split-export-declaration": "7.22.6", - "@babel/plugin-transform-async-generator-functions": "7.23.9", - "@babel/plugin-transform-async-to-generator": "7.23.3", - "@babel/plugin-transform-runtime": "7.24.0", - "@babel/preset-env": "7.24.0", - "@babel/runtime": "7.24.0", - "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.3.6", - "@vitejs/plugin-basic-ssl": "1.1.0", + "@angular-devkit/architect": "0.1902.9", + "@angular-devkit/build-webpack": "0.1902.9", + "@angular-devkit/core": "19.2.9", + "@angular/build": "19.2.9", + "@babel/core": "7.26.10", + "@babel/generator": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.26.8", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.26.10", + "@babel/preset-env": "7.26.9", + "@babel/runtime": "7.26.10", + "@discoveryjs/json-ext": "0.6.3", + "@ngtools/webpack": "19.2.9", + "@vitejs/plugin-basic-ssl": "1.2.0", "ansi-colors": "4.1.3", - "autoprefixer": "10.4.18", - "babel-loader": "9.1.3", - "babel-plugin-istanbul": "6.1.1", + "autoprefixer": "10.4.20", + "babel-loader": "9.2.1", "browserslist": "^4.21.5", - "copy-webpack-plugin": "11.0.0", - "critters": "0.0.22", - "css-loader": "6.10.0", - "esbuild-wasm": "0.20.1", - "fast-glob": "3.3.2", - "http-proxy-middleware": "2.0.6", - "https-proxy-agent": "7.0.4", - "inquirer": "9.2.15", - "jsonc-parser": "3.2.1", + "copy-webpack-plugin": "12.0.2", + "css-loader": "7.1.2", + "esbuild-wasm": "0.25.1", + "fast-glob": "3.3.3", + "http-proxy-middleware": "3.0.5", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", "karma-source-map-support": "1.4.0", - "less": "4.2.0", - "less-loader": "11.1.0", + "less": "4.2.2", + "less-loader": "12.2.0", "license-webpack-plugin": "4.0.2", - "loader-utils": "3.2.1", - "magic-string": "0.30.8", - "mini-css-extract-plugin": "2.8.1", - "mrmime": "2.0.0", - "open": "8.4.2", + "loader-utils": "3.3.1", + "mini-css-extract-plugin": "2.9.2", + "open": "10.1.0", "ora": "5.4.1", - "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "4.0.1", - "piscina": "4.4.0", - "postcss": "8.4.35", + "picomatch": "4.0.2", + "piscina": "4.8.0", + "postcss": "8.5.2", "postcss-loader": "8.1.1", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", - "sass": "1.71.1", - "sass-loader": "14.1.1", - "semver": "7.6.0", + "sass": "1.85.0", + "sass-loader": "16.0.5", + "semver": "7.7.1", "source-map-loader": "5.0.0", "source-map-support": "0.5.21", - "terser": "5.29.1", + "terser": "5.39.0", "tree-kill": "1.2.2", - "tslib": "2.6.2", - "undici": "6.11.1", - "vite": "5.1.7", - "watchpack": "2.4.0", - "webpack": "5.90.3", - "webpack-dev-middleware": "6.1.2", - "webpack-dev-server": "4.15.1", - "webpack-merge": "5.10.0", + "tslib": "2.8.1", + "webpack": "5.98.0", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.2.0", + "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "optionalDependencies": { - "esbuild": "0.20.1" + "esbuild": "0.25.1" }, "peerDependencies": { - "@angular/compiler-cli": "^17.0.0", - "@angular/localize": "^17.0.0", - "@angular/platform-server": "^17.0.0", - "@angular/service-worker": "^17.0.0", - "@web/test-runner": "^0.18.0", + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "@angular/localize": "^19.0.0 || ^19.2.0-next.0", + "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", + "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", + "@angular/ssr": "^19.2.9", + "@web/test-runner": "^0.20.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "karma": "^6.3.0", - "ng-packagr": "^17.0.0", + "ng-packagr": "^19.0.0 || ^19.2.0-next.0", "protractor": "^7.0.0", - "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.2 <5.5" + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.9" }, "peerDependenciesMeta": { "@angular/localize": { @@ -183,6 +192,9 @@ "@angular/service-worker": { "optional": true }, + "@angular/ssr": { + "optional": true + }, "@web/test-runner": { "optional": true }, @@ -209,45 +221,67 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1703.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.6.tgz", - "integrity": "sha512-pJu0et2SiF0kfXenHSTtAART0omzbWpLgBfeUo4hBh4uwX5IaT+mRpYpr8gCXMq+qsjoQp3HobSU3lPDeBn+bg==", + "version": "0.1902.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.9.tgz", + "integrity": "sha512-iklNoxKgwd54KT5GE0o5SB+0hr6Iu3YSpj9fi23DlLKcWWwFYaKqoRaYcfuL7KdUzunFg7dzB7n6TgYpVHWWJw==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1703.6", + "@angular-devkit/architect": "0.1902.9", "rxjs": "7.8.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "peerDependencies": { "webpack": "^5.30.0", - "webpack-dev-server": "^4.0.0" + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" } }, "node_modules/@angular-devkit/core": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.6.tgz", - "integrity": "sha512-FVbkT9dEwHEvjnxr4mvMNSMg2bCFoGoP4X68xXU9dhLEUpC05opLvfbaR3Qh543eCJ5AstosBFVzB/krfIkOvA==", + "version": "19.2.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.9.tgz", + "integrity": "sha512-vbTomKnN7H4jaif0hWAECFU2WvRbhfkYWHdlk/JtJM53iIJVL3mKWBRZ0QXITjmgfdIo3c9RcX+wFI7gGqGd6g==", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "peerDependencies": { - "chokidar": "^3.5.2" + "chokidar": "^4.0.0" }, "peerDependenciesMeta": { "chokidar": { @@ -255,231 +289,401 @@ } } }, - "node_modules/@angular-devkit/schematics": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.10.tgz", - "integrity": "sha512-hjf4gaMx2uB6ZhBstBSH0Q2hzfp6kxI4IiJ5i1QrxPNE1MdGnb2h+LgPTRCdO72a7PGeWcSxFRE7cxrXeQy19g==", + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@angular-devkit/core": "17.0.10", - "jsonc-parser": "3.2.0", - "magic-string": "0.30.5", - "ora": "5.4.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "tslib": "^2.1.0" } }, - "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.10.tgz", - "integrity": "sha512-93N6oHnmtRt0hL3AXxvnk47sN1rHndfj+pqI5haEY41AGWzIWv9cSBsqlM0PWltNpo6VivcExZESvbLJ71wqbQ==", + "node_modules/@angular-devkit/schematics": { + "version": "19.2.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.9.tgz", + "integrity": "sha512-B8FQ4hFsP4Ffh895F9GVvyhgDoZztWnAyYKiM1pyvLSQikzaUZqi9NZnD12HgMALmwm2z36zTzoSNsYFBTHgaw==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "picomatch": "3.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" + "@angular-devkit/core": "19.2.9", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } } }, - "node_modules/@angular-devkit/schematics/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "node_modules/@angular-devkit/schematics/node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, + "license": "Apache-2.0", + "peer": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "tslib": "^2.1.0" } }, "node_modules/@angular-eslint/builder": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-17.2.0.tgz", - "integrity": "sha512-xPxgCTPcnFRT8OYs9R5UZVAtzVouIIfdMOqTcB847Cev4H8kqRz0gO5aqkQiL+0erwnLf8D4nRzMTJjSBpQQNw==", + "version": "19.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.0.0-alpha.4.tgz", + "integrity": "sha512-iSDl0Hs2fkJJH0aR/RQ80nmickY7o1xv+mucSw/Gy4YwFDJFU0FiV++1OxhjturuEXt3k+TJ115xe4DJa86BMw==", "dev": true, - "dependencies": { - "@nx/devkit": "17.2.8", - "nx": "17.2.8" - }, + "license": "MIT", "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.2.0.tgz", - "integrity": "sha512-uBvPbPE2JxqpdLs//Nd5+TRLgjxDxvTYgmGFTKI9Eo98krqps+rhSQCRSHWACukzc25X3Q4ITHfvjODQL8qQkg==", - "dev": true + "version": "19.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.0.0-alpha.4.tgz", + "integrity": "sha512-SS2FHqRaGslJzI+cTBNDC7xg/Zx5c0iIXZnpwGa8VjJ/8L82+PlRS+d9CTBhb8tMsR06ifUTK9ym2JQ3VmE2Cg==", + "dev": true, + "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.2.0.tgz", - "integrity": "sha512-8A3hD/11N6QEchsAGggTPmNsa0GS5p44t930slMsxrTvdSlKAo56FzVdxwSkOcejKIJs57oWxoKvtK4UyLYkeA==", + "version": "19.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.0.0-alpha.4.tgz", + "integrity": "sha512-IhBeiUohYLsnUrSJ92riSrhfIkGefuXIrGTgBnagn887WFR45/Go5dIIivVfMGdvTg849dtLpBDXZrycHY3QFA==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-eslint/utils": "17.2.0", - "@typescript-eslint/utils": "6.18.0" + "@angular-eslint/bundled-angular-compiler": "19.0.0-alpha.4", + "@angular-eslint/utils": "19.0.0-alpha.4" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.2.0.tgz", - "integrity": "sha512-CkcAOWWqNwX0FXeLwJu0Vctso8q/NPHJ95R2Cy8hjwuMyFF83/vDouyeIjYC+SRv6hbevmNa+BbdYXhQZinIHw==", + "version": "19.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.0.0-alpha.4.tgz", + "integrity": "sha512-YiFB+tyTZ/mj/w/5DLJHl9J1ABGaHNhGdXJzagI0ufqyrePR0wTYMIyJpIGWOMHr9E5nYPsVuHn+d08dG1R0aQ==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.2.0", - "@angular-eslint/utils": "17.2.0", - "@typescript-eslint/type-utils": "6.18.0", - "@typescript-eslint/utils": "6.18.0", - "aria-query": "5.3.0", - "axobject-query": "4.0.0" + "@angular-eslint/bundled-angular-compiler": "19.0.0-alpha.4", + "@angular-eslint/utils": "19.0.0-alpha.4", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@typescript-eslint/types": "^7.11.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/schematics": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-17.2.0.tgz", - "integrity": "sha512-lV2+2H3Hf6FCJfM+cddJ/7ss3qc99OO2wuvTjGNH512mP75tvfLakV+e6TFFdzK0km+ceXvB2VqNXMSShB4PVQ==", + "version": "19.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.0.0-alpha.4.tgz", + "integrity": "sha512-Xv8g2PbNqhm7igTnY3uuY511Fr+FVRBHG+ZHbcUPZT7YDBaFZRJezAzbbPLZ6GdqqhoQJPHdGuEvo22yKhKXag==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-eslint/eslint-plugin": "17.2.0", - "@angular-eslint/eslint-plugin-template": "17.2.0", - "@nx/devkit": "17.2.8", - "ignore": "5.3.0", - "nx": "17.2.8", - "strip-json-comments": "3.1.1", - "tmp": "0.2.1" + "@angular-eslint/eslint-plugin": "19.0.0-alpha.4", + "@angular-eslint/eslint-plugin-template": "19.0.0-alpha.4", + "ignore": "6.0.2", + "semver": "7.6.3", + "strip-json-comments": "3.1.1" }, "peerDependencies": { - "@angular/cli": ">= 17.0.0 < 18.0.0" + "@angular-devkit/core": ">= 19.0.0 < 20.0.0", + "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/@angular-eslint/template-parser": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-17.2.0.tgz", - "integrity": "sha512-Js9w1IXWPvXEjd05bWkZzRaLw0g0mJPztAWOj3DiU7H9LKkautQq0zZu02cAAnXZim2CsAagEh2GmGjhaYvoKg==", + "version": "19.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.0.0-alpha.4.tgz", + "integrity": "sha512-Mvy1kbnqoYBQFFpQtmBB/TkhmmoN97ruSv9xa3mpKzv8JlDdVCkIn7IdqLtzcLwGr+MGcPC7GFPl8o7q12N3BQ==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.2.0", - "eslint-scope": "^8.0.0" + "@angular-eslint/bundled-angular-compiler": "19.0.0-alpha.4", + "eslint-scope": "^8.0.2" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/utils": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.2.0.tgz", - "integrity": "sha512-J7DsFKb5yxv8te8LQvChNn6MBvKulcBx+jtHX1uen+uuuv8XhZuVMZXS0rolUkdl1Q0mBeHpkuO2q6Vh17pqbQ==", + "version": "19.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.0.0-alpha.4.tgz", + "integrity": "sha512-6Pxqs3QqSPBcAkP8I/GYijoPoAmqOYqyQvJGvBWd1oKlA3EqmXSi7uaSUa6nUI6BiA8JEJZTBaSOP6O2oyK25Q==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.2.0", - "@typescript-eslint/utils": "6.18.0" + "@angular-eslint/bundled-angular-compiler": "19.0.0-alpha.4" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular/animations": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.7.tgz", - "integrity": "sha512-ahenGALPPweeHgqtl9BMkGIAV4fUNI5kOWUrLNbKBfwIJN+aOBOYV1Jz6NKUQq6eYn/1ZYtm0f3lIkHIdtLKEw==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.8.tgz", + "integrity": "sha512-gKWBusQvjb946uuTXaXWzkEfLdTiy9GUNZ9okF3yolv+aoW0D8AM9mVvTX1xdqAV3xuIxRXRbkWG7BR+p8xVzQ==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "17.3.7" + "@angular/common": "19.2.8", + "@angular/core": "19.2.8" } }, - "node_modules/@angular/cdk": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.7.tgz", - "integrity": "sha512-aFEh8tzKFOwini6aNEp57S54Ocp9T7YIJfBVMESptu2TCPdMTlJ1HJTg5XS8NcQO+vwi9cFPGVwGF1frOx4LXA==", + "node_modules/@angular/build": { + "version": "19.2.9", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.9.tgz", + "integrity": "sha512-hrRhSdY98wGQ/jrpT3K73/Ii5FadQEJFcHy+ockqP2Xh7pXOwhGFc+D0ks4AdHea+pHtNbIb/qPd+UvR5izY3Q==", + "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^2.3.0" + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1902.9", + "@babel/core": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@inquirer/confirm": "5.1.6", + "@vitejs/plugin-basic-ssl": "1.2.0", + "beasties": "0.3.2", + "browserslist": "^4.23.0", + "esbuild": "0.25.1", + "fast-glob": "3.3.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "listr2": "8.2.5", + "magic-string": "0.30.17", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.8.0", + "rollup": "4.34.8", + "sass": "1.85.0", + "semver": "7.7.1", + "source-map-support": "0.5.21", + "vite": "6.2.6", + "watchpack": "2.4.2" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, "optionalDependencies": { - "parse5": "^7.1.2" + "lmdb": "3.2.6" }, "peerDependencies": { - "@angular/common": "^17.0.0 || ^18.0.0", - "@angular/core": "^17.0.0 || ^18.0.0", - "rxjs": "^6.5.3 || ^7.4.0" + "@angular/compiler": "^19.0.0 || ^19.2.0-next.0", + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "@angular/localize": "^19.0.0 || ^19.2.0-next.0", + "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", + "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", + "@angular/ssr": "^19.2.9", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^19.0.0 || ^19.2.0-next.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.9" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } } }, - "node_modules/@angular/cli": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.10.tgz", - "integrity": "sha512-52rd8KmOMe3NJDp/wA+Mwj21qd4HR8fuLtfrErgVnZaJZKX2Bzi/z7FHQD3gdgMAdzUiG0OJWGM0h75Ls9X6Gw==", + "node_modules/@angular/build/node_modules/vite": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", + "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/vite/node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@angular/cdk": { + "version": "19.2.11", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.11.tgz", + "integrity": "sha512-G568yWIJlnsuS563WxvCofmxc1405+wRQvDGQ32+qWOblJScFkHgr4jeDkZGcyt/r8OudaW0H0/rNeg1dzdnIQ==", + "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1700.10", - "@angular-devkit/core": "17.0.10", - "@angular-devkit/schematics": "17.0.10", - "@schematics/angular": "17.0.10", + "parse5": "^7.1.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^19.0.0 || ^20.0.0", + "@angular/core": "^19.0.0 || ^20.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.0.7.tgz", + "integrity": "sha512-y6C4B4XdiZwe2+OADLWXyKqUVvW/XDzTuJ2mZ5PhTnSiiXDN4zRWId1F5wA8ve8vlbUKApPHXRQuaqiQJmA24g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1900.7", + "@angular-devkit/core": "19.0.7", + "@angular-devkit/schematics": "19.0.7", + "@inquirer/prompts": "7.1.0", + "@listr2/prompt-adapter-inquirer": "2.0.18", + "@schematics/angular": "19.0.7", "@yarnpkg/lockfile": "1.1.0", - "ansi-colors": "4.1.3", - "ini": "4.1.1", - "inquirer": "9.2.11", - "jsonc-parser": "3.2.0", - "npm-package-arg": "11.0.1", - "npm-pick-manifest": "9.0.0", - "open": "8.4.2", - "ora": "5.4.1", - "pacote": "17.0.4", + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "8.2.5", + "npm-package-arg": "12.0.0", + "npm-pick-manifest": "10.0.0", + "pacote": "20.0.0", "resolve": "1.22.8", - "semver": "7.5.4", + "semver": "7.6.3", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, @@ -487,46 +691,48 @@ "ng": "bin/ng.js" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { - "version": "0.1700.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.10.tgz", - "integrity": "sha512-JD/3jkdN1jrFMIDEk9grKdbjutIoxUDMRazq1LZooWjTkzlYk09i/s6HwvIPao7zvxJfelD6asTPspgkjOMP5A==", + "version": "0.1900.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1900.7.tgz", + "integrity": "sha512-3dRV0IB+MbNYbAGbYEFMcABkMphqcTvn5MG79dQkwcf2a9QZxCq2slwf/rIleWoDUcFm9r1NnVPYrTYNYJaqQg==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "17.0.10", + "@angular-devkit/core": "19.0.7", "rxjs": "7.8.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.10.tgz", - "integrity": "sha512-93N6oHnmtRt0hL3AXxvnk47sN1rHndfj+pqI5haEY41AGWzIWv9cSBsqlM0PWltNpo6VivcExZESvbLJ71wqbQ==", + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.0.7.tgz", + "integrity": "sha512-VyuORSitT6LIaGUEF0KEnv2TwNaeWl6L3/4L4stok0BJ23B4joVca2DYVcrLC1hSzz8V4dwVgSlbNIgjgGdVpg==", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "picomatch": "3.0.1", + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "peerDependencies": { - "chokidar": "^3.5.2" + "chokidar": "^4.0.0" }, "peerDependenciesMeta": { "chokidar": { @@ -534,178 +740,96 @@ } } }, - "node_modules/@angular/cli/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "node_modules/@angular/cli/node_modules/@angular-devkit/schematics": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.0.7.tgz", + "integrity": "sha512-BHXQv6kMc9xo4TH9lhwMv8nrZXHkLioQvLun2qYjwvOsyzt3qd+sUM9wpHwbG6t+01+FIQ05iNN9ox+Cvpndgg==", "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.0.7", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.12", + "ora": "5.4.1", + "rxjs": "7.8.1" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "node_modules/@angular/cli/node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/@angular/cli/node_modules/figures": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", - "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", + "node_modules/@angular/cli/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "escape-string-regexp": "^5.0.0", - "is-unicode-supported": "^1.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "tslib": "^2.1.0" } }, - "node_modules/@angular/cli/node_modules/inquirer": { - "version": "9.2.11", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.11.tgz", - "integrity": "sha512-B2LafrnnhbRzCWfAdOXisUzL89Kg8cVJlYmhqoi3flSiV/TveO+nsXwgKr9h9PIo+J1hz7nBSk6gegRIMBBf7g==", + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "@ljharb/through": "^2.3.9", - "ansi-escapes": "^4.3.2", - "chalk": "^5.3.0", - "cli-cursor": "^3.1.0", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "figures": "^5.0.0", - "lodash": "^4.17.21", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=14.18.0" + "node": ">=10" } }, - "node_modules/@angular/cli/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, + "node_modules/@angular/common": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.8.tgz", + "integrity": "sha512-SnW+/amz1Mtni9125xlzPZ5MU+wSzUepc9G5jRnL0q9vrFglRWa3BEW3GxVurfbdnf6FleroZ7fZCZFAfREw7Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, "engines": { - "node": ">=12" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular/cli/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "node_modules/@angular/cli/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular/cli/node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@angular/cli/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular/cli/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@angular/common": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.7.tgz", - "integrity": "sha512-A7LRJu1vVCGGgrfZXjU+njz50SiU4weheKCar5PIUprcdIofS1IrHAJDqYh+kwXxkjXbZMOr/ijQY0+AESLEsw==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/core": "17.3.7", - "rxjs": "^6.5.3 || ^7.4.0" + "peerDependencies": { + "@angular/core": "19.2.8", + "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.7.tgz", - "integrity": "sha512-AlKiqPoxnrpQ0hn13fIaQPSVodaVAIjBW4vpFyuKFqs2LBKg6iolwZ21s8rEI0KR2gXl+8ugj0/UZ6YADiVM5w==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.8.tgz", + "integrity": "sha512-HBtt96X09XFatHAnkquFYbcD3aQSvuYoqqhCV5OLkhAwHmvr3BGyHx/EBZ5JGOfCNOzCupoQmOBF+nh5LKwkeQ==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/core": "17.3.7" - }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - } + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" } }, "node_modules/@angular/compiler-cli": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.7.tgz", - "integrity": "sha512-vSg5IQZ9jGmvYjpbfH8KbH4Sl1IVeE+Mr1ogcxkGEsURSRvKo7EWc0K7LSEI9+gg0VLamMiP9EyCJdPxiJeLJQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.8.tgz", + "integrity": "sha512-gq/sc3D3m6aKmhdSTTzzD59wfQcVjIZ8dgJoPW7pOcmPVQL1N8syjv+quHySfSJlBkbs5dQ0P4Kk0yvxRw9S7g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/core": "7.23.9", + "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^3.0.0", + "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", "reflect-metadata": "^0.2.0", "semver": "^7.0.0", @@ -718,29 +842,30 @@ "ngcc": "bundles/ngcc/index.js" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "17.3.7", - "typescript": ">=5.2 <5.5" + "@angular/compiler": "19.2.8", + "typescript": ">=5.5 <5.9" } }, "node_modules/@angular/compiler-cli/node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -759,127 +884,85 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@angular/core": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.7.tgz", - "integrity": "sha512-HWcrbxqnvIMSxFuQdN0KPt08bc87hqr0LKm89yuRTUwx/2sNJlNQUobk6aJj4trswGBttcRDT+GOS4DQP2Nr4g==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.8.tgz", + "integrity": "sha512-iNISGgLr+nBzEaGbfzRCOVfV3T66gbEu+Ee4VCnEqifU7Er6fnvn+oFfHo3gNKHrCdicrbyb2oKAmeOJynKbsA==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.14.0" + "zone.js": "~0.15.0" } }, "node_modules/@angular/forms": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.7.tgz", - "integrity": "sha512-FEhXh/VmT++XCoO8i7bBtzxG7Am/cE1zrr9aF+fWW+4jpWvJvVN1IaSiJxgBB+iPsOJ9lTBRwfRW3onlcDkhrw==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.8.tgz", + "integrity": "sha512-4q/6ad8YZPixxLhDwOxm4pQO3ekwGriOTVB0pMb9FdpvjOUSdDTM08o8ToHvu6MBbZjHzLs8+xkMw9QCd55x/w==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "17.3.7", - "@angular/core": "17.3.7", - "@angular/platform-browser": "17.3.7", + "@angular/common": "19.2.8", + "@angular/core": "19.2.8", + "@angular/platform-browser": "19.2.8", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/material": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.7.tgz", - "integrity": "sha512-wjSKkk9KZE8QiBPkMd5axh5u/3pUSxoLKNO7OasFhEagMmSv5oYTLm40cErhtb4UdkSmbC19WuuluS6P3leoPA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/auto-init": "15.0.0-canary.7f224ddd4.0", - "@material/banner": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/card": "15.0.0-canary.7f224ddd4.0", - "@material/checkbox": "15.0.0-canary.7f224ddd4.0", - "@material/chips": "15.0.0-canary.7f224ddd4.0", - "@material/circular-progress": "15.0.0-canary.7f224ddd4.0", - "@material/data-table": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dialog": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/drawer": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/fab": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/form-field": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/image-list": "15.0.0-canary.7f224ddd4.0", - "@material/layout-grid": "15.0.0-canary.7f224ddd4.0", - "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", - "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu": "15.0.0-canary.7f224ddd4.0", - "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", - "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", - "@material/radio": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/segmented-button": "15.0.0-canary.7f224ddd4.0", - "@material/select": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/slider": "15.0.0-canary.7f224ddd4.0", - "@material/snackbar": "15.0.0-canary.7f224ddd4.0", - "@material/switch": "15.0.0-canary.7f224ddd4.0", - "@material/tab": "15.0.0-canary.7f224ddd4.0", - "@material/tab-bar": "15.0.0-canary.7f224ddd4.0", - "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", - "@material/textfield": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tooltip": "15.0.0-canary.7f224ddd4.0", - "@material/top-app-bar": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", + "version": "19.2.11", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.2.11.tgz", + "integrity": "sha512-0OWwv55Il25mit7oGTloMeKVi0v/q1tr13wUJj0KJOcvICA6JCEW6VEc9zqYmkMPstDCx96cSJgPKxkHjKYyqg==", + "license": "MIT", + "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/animations": "^17.0.0 || ^18.0.0", - "@angular/cdk": "17.3.7", - "@angular/common": "^17.0.0 || ^18.0.0", - "@angular/core": "^17.0.0 || ^18.0.0", - "@angular/forms": "^17.0.0 || ^18.0.0", - "@angular/platform-browser": "^17.0.0 || ^18.0.0", + "@angular/cdk": "19.2.11", + "@angular/common": "^19.0.0 || ^20.0.0", + "@angular/core": "^19.0.0 || ^20.0.0", + "@angular/forms": "^19.0.0 || ^20.0.0", + "@angular/platform-browser": "^19.0.0 || ^20.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.7.tgz", - "integrity": "sha512-Nn8ZMaftAvO9dEwribWdNv+QBHhYIBrRkv85G6et80AXfXoYAr/xcfnQECRFtZgPmANqHC5auv/xrmExQG+Yeg==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.8.tgz", + "integrity": "sha512-3O69vMAq/ki13YX8hWBUs1R6iwS1GmkcHWu5fIUU7rjSuhGfD60nASqRBYZiJb68eUom//T544KavOvfAl1PzQ==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "17.3.7", - "@angular/common": "17.3.7", - "@angular/core": "17.3.7" + "@angular/animations": "19.2.8", + "@angular/common": "19.2.8", + "@angular/core": "19.2.8" }, "peerDependenciesMeta": { "@angular/animations": { @@ -888,46 +971,50 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.7.tgz", - "integrity": "sha512-9c2I4u0L1p2v1/lW8qy+WaNHisUWbyy6wqsv2v9FfCaSM49Lxymgo9LPFPC4qEG5ei5nE+eIQ2ocRiXXsf5QkQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.8.tgz", + "integrity": "sha512-Vwh53CGCC/I3DQ/nqWxNTKk052CRHv46H6KjfWBsD8vOVTJoQf2HXwEbDKntpmJ0K4MtMdIdbpwXieUMLyfmXA==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "17.3.7", - "@angular/compiler": "17.3.7", - "@angular/core": "17.3.7", - "@angular/platform-browser": "17.3.7" + "@angular/common": "19.2.8", + "@angular/compiler": "19.2.8", + "@angular/core": "19.2.8", + "@angular/platform-browser": "19.2.8" } }, "node_modules/@angular/router": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.7.tgz", - "integrity": "sha512-lMkuRrc1ZjP5JPDxNHqoAhB0uAnfPQ/q6mJrw1s8IZoVV6VyM+FxR5r13ajNcXWC38xy/YhBjpXPF1vBdxuLXg==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.8.tgz", + "integrity": "sha512-aZenxUzrz8idGmw0jsVaPFY8EAPOYcOHmv9mDljzAhJZHaSX/r0iVasnjf5qUkTb7ElpRXppS4wXPNNGKTrXZA==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "17.3.7", - "@angular/core": "17.3.7", - "@angular/platform-browser": "17.3.7", + "@angular/common": "19.2.8", + "@angular/core": "19.2.8", + "@angular/platform-browser": "19.2.8", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -935,30 +1022,32 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", + "@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", @@ -977,65 +1066,59 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", + "@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" }, @@ -1048,24 +1131,24 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", - "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.24.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz", + "integrity": "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==", + "dev": true, + "license": "MIT", + "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" }, "engines": { @@ -1075,35 +1158,25 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.0.tgz", + "integrity": "sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { @@ -1118,15 +1191,17 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", - "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", + "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", @@ -1138,75 +1213,44 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", - "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1215,48 +1259,39 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1266,14 +1301,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", - "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1282,104 +1318,100 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.5.tgz", - "integrity": "sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-function-name": "^7.23.0", - "@babel/template": "^7.24.0", - "@babel/types": "^7.24.5" + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", - "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.25.9", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -1388,11 +1420,93 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -1400,13 +1514,47 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", - "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1416,14 +1564,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", - "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.24.1" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1433,13 +1582,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", - "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1453,6 +1603,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" }, @@ -1460,37 +1611,30 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.25.9" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1499,37 +1643,49 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", - "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" }, "engines": { "node": ">=6.9.0" @@ -1538,13 +1694,16 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", - "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1553,109 +1712,151 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.0.tgz", + "integrity": "sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.12.0" } }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@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" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1664,29 +1865,15 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1695,223 +1882,14 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", - "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", - "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", - "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.5.tgz", - "integrity": "sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", - "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", - "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.4", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.5.tgz", - "integrity": "sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-split-export-declaration": "^7.24.5", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", - "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/template": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.5.tgz", - "integrity": "sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", - "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", - "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", - "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1921,13 +1899,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", - "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1937,13 +1915,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", - "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1953,13 +1931,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", - "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", + "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1969,14 +1948,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", - "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1986,13 +1966,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", - "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2002,12 +1982,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", - "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2017,13 +1998,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", - "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2033,12 +2014,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", - "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2048,13 +2030,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", - "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2064,14 +2047,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", - "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2081,15 +2064,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", - "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-identifier": "^7.22.20" + "@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" }, "engines": { "node": ">=6.9.0" @@ -2099,13 +2083,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", - "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2115,13 +2100,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2131,12 +2117,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", - "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2146,13 +2133,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", - "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -2162,13 +2149,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", - "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2178,15 +2165,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.5.tgz", - "integrity": "sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.5" + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2196,13 +2183,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", - "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-replace-supers": "^7.24.1" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2212,13 +2200,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", - "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2228,14 +2216,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.5.tgz", - "integrity": "sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2245,12 +2233,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.5.tgz", - "integrity": "sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2260,13 +2249,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", - "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2276,15 +2266,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", - "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.5", - "@babel/helper-plugin-utils": "^7.24.5", - "@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" }, "engines": { "node": ">=6.9.0" @@ -2294,12 +2284,13 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", - "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2309,12 +2300,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", - "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.0.tgz", + "integrity": "sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.26.5", "regenerator-transform": "^0.15.2" }, "engines": { @@ -2324,13 +2316,31 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", - "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2340,16 +2350,17 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz", - "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", + "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "engines": { @@ -2364,17 +2375,19 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", - "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2384,13 +2397,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", - "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2400,12 +2414,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", - "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2415,12 +2430,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", - "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", + "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -2430,12 +2446,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.5.tgz", - "integrity": "sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.0.tgz", + "integrity": "sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -2445,12 +2462,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", - "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2460,13 +2478,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", - "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2476,13 +2495,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", - "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2492,13 +2512,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", - "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2508,90 +2529,80 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", - "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "dev": true, + "license": "MIT", + "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.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", - "@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.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.9", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.8", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.9", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.24.0", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@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.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", - "core-js-compat": "^3.31.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "engines": { @@ -2606,6 +2617,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -2615,6 +2627,7 @@ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", @@ -2624,17 +2637,12 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true - }, "node_modules/@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dev": true, + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2643,33 +2651,32 @@ } }, "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "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" }, @@ -2678,41 +2685,31 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", - "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.5", + "@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" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2723,407 +2720,471 @@ "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.1.90" } }, "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.17.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", - "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", - "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", - "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", - "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", - "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", - "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", - "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", - "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", - "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", - "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", - "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", - "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", - "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", - "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", - "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", - "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", - "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", - "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", - "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", - "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", - "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", - "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", - "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -3133,6 +3194,7 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -3156,6 +3218,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3167,17 +3230,12 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3188,6 +3246,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -3198,29 +3257,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3233,6 +3292,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -3241,21 +3301,24 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -3268,6 +3331,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3278,6 +3342,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3290,6 +3355,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -3302,13 +3368,329 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@inquirer/checkbox": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.5.tgz", + "integrity": "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.6.tgz", + "integrity": "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz", + "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.10.tgz", + "integrity": "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.12.tgz", + "integrity": "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", + "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.9.tgz", + "integrity": "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.12.tgz", + "integrity": "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.12.tgz", + "integrity": "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.1.0.tgz", + "integrity": "sha512-5U/XiVRH2pp1X6gpNAjWOglMf38/Ys522ncEHIKT1voRUvSj/DQnR22OVxHnwu5S+rCFaUiPQ57JOtMFQayqYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.0.2", + "@inquirer/confirm": "^5.0.2", + "@inquirer/editor": "^4.1.0", + "@inquirer/expand": "^4.0.2", + "@inquirer/input": "^4.0.2", + "@inquirer/number": "^3.0.2", + "@inquirer/password": "^4.0.2", + "@inquirer/rawlist": "^4.0.2", + "@inquirer/search": "^3.0.2", + "@inquirer/select": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.0.tgz", + "integrity": "sha512-6ob45Oh9pXmfprKqUiEeMz/tjtVTFQTgDDz1xAMKMrIvyrYjAmRbQZjMJfsictlL4phgjLhdLu27IkHNnNjB7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.12.tgz", + "integrity": "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.0.tgz", + "integrity": "sha512-KkXQ4aSySWimpV4V/TUJWdB3tdfENZUU765GjOIZ0uPwdbGIG6jrxD4dDf1w68uP+DVtfNhr1A92B+0mbTZ8FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", + "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -3322,10 +3704,11 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3338,6 +3721,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3349,13 +3733,15 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -3373,6 +3759,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -3388,6 +3775,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -3400,20 +3788,17 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, + "license": "ISC", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "minipass": "^7.0.4" }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, "node_modules/@istanbuljs/schema": { @@ -3421,27 +3806,17 @@ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -3456,6 +3831,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -3465,6 +3841,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -3474,860 +3851,690 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz", + "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/@ljharb/through": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", - "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.18.tgz", + "integrity": "sha512-0hz44rAcrphyXcA8IS7EJ2SCoaBZD2u5goE8S/e+q/DL+dOGpqpcLidVOFeLG3VgML62SXmfRLAhWt0zL1oW4Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "@inquirer/type": "^1.5.5" }, "engines": { - "node": ">= 0.4" + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8" } }, - "node_modules/@material/animation": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-1GSJaPKef+7HRuV+HusVZHps64cmZuOItDbt40tjJVaikcaZvwmHlcTxRIqzcRoCdt5ZKHh3NoO7GB9Khg4Jnw==", + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@material/auto-init": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-t7ZGpRJ3ec0QDUO0nJu/SMgLW7qcuG2KqIsEYD1Ej8qhI2xpdR2ydSDQOkVEitXmKoGol1oq4nYSBjTlB65GqA==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/banner": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-g9wBUZzYBizyBcBQXTIafnRUUPi7efU9gPJfzeGgkynXiccP/vh5XMmH+PBxl5v+4MlP/d4cZ2NUYoAN7UTqSA==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/base": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-I9KQOKXpLfJkP8MqZyr8wZIzdPHrwPjFvGd9zSK91/vPyE4hzHRJc/0njsh9g8Lm9PRYLbifXX+719uTbHxx+A==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@material/button": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-BHB7iyHgRVH+JF16+iscR+Qaic+p7LU1FOLgP8KucRlpF9tTwIxQA6mJwGRi5gUtcG+vyCmzVS+hIQ6DqT/7BA==", - "dependencies": { - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/card": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-kt7y9/IWOtJTr3Z/AoWJT3ZLN7CLlzXhx2udCLP9ootZU2bfGK0lzNwmo80bv/pJfrY9ihQKCtuGTtNxUy+vIw==", - "dependencies": { - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/checkbox": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-rURcrL5O1u6hzWR+dNgiQ/n89vk6tdmdP3mZgnxJx61q4I/k1yijKqNJSLrkXH7Rto3bM5NRKMOlgvMvVd7UMQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/chips": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-AYAivV3GSk/T/nRIpH27sOHFPaSMrE3L0WYbnb5Wa93FgY8a0fbsFYtSH2QmtwnzXveg+B1zGTt7/xIIcynKdQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/checkbox": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "safevalues": "^0.3.4", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/circular-progress": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-DJrqCKb+LuGtjNvKl8XigvyK02y36GRkfhMUYTcJEi3PrOE00bwXtyj7ilhzEVshQiXg6AHGWXtf5UqwNrx3Ow==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/data-table": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-/2WZsuBIq9z9RWYF5Jo6b7P6u0fwit+29/mN7rmAZ6akqUR54nXyNfoSNiyydMkzPlZZsep5KrSHododDhBZbA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/checkbox": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/select": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/density": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-o9EXmGKVpiQ6mHhyV3oDDzc78Ow3E7v8dlaOhgaDSXgmqaE8v5sIlLNa/LKSyUga83/fpGk3QViSGXotpQx0jA==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@material/dialog": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-u0XpTlv1JqWC/bQ3DavJ1JguofTelLT2wloj59l3/1b60jv42JQ6Am7jU3I8/SIUB1MKaW7dYocXjDWtWJakLA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/dom": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-mQ1HT186GPQSkRg5S18i70typ5ZytfjL09R0gJ2Qg5/G+MLCGi7TAjZZSH65tuD/QGOjel4rDdWOTmYbPYV6HA==", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/drawer": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-qyO0W0KBftfH8dlLR0gVAgv7ZHNvU8ae11Ao6zJif/YxcvK4+gph1z8AO4H410YmC2kZiwpSKyxM1iQCCzbb4g==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/elevation": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-tV6s4/pUBECedaI36Yj18KmRCk1vfue/JP/5yYRlFNnLMRVISePbZaKkn/BHXVf+26I3W879+XqIGlDVdmOoMA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/fab": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-4h76QrzfZTcPdd+awDPZ4Q0YdSqsXQnS540TPtyXUJ/5G99V6VwGpjMPIxAsW0y+pmI9UkLL/srrMaJec+7r4Q==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/feature-targeting": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-SAjtxYh6YlKZriU83diDEQ7jNSP2MnxKsER0TvFeyG1vX/DWsUyYDOIJTOEa9K1N+fgJEBkNK8hY55QhQaspew==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@material/floating-label": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-0KMo5ijjYaEHPiZ2pCVIcbaTS2LycvH9zEhEMKwPPGssBCX7iz5ffYQFk7e5yrQand1r3jnQQgYfHAwtykArnQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/focus-ring": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-Jmg1nltq4J6S6A10EGMZnvufrvU3YTi+8R8ZD9lkSbun0Fm2TVdICQt/Auyi6An9zP66oQN6c31eqO6KfIPsDg==", - "dependencies": { - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0" - } - }, - "node_modules/@material/form-field": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-fEPWgDQEPJ6WF7hNnIStxucHR9LE4DoDSMqCsGWS2Yu+NLZYLuCEecgR0UqQsl1EQdNRaFh8VH93KuxGd2hiPg==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/icon-button": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-DcK7IL4ICY/DW+48YQZZs9g0U1kRaW0Wb0BxhvppDMYziHo/CTpFdle4gjyuTyRxPOdHQz5a97ru48Z9O4muTw==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@material/image-list": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-voMjG2p80XbjL1B2lmF65zO5gEgJOVKClLdqh4wbYzYfwY/SR9c8eLvlYG7DLdFaFBl/7gGxD8TvvZ329HUFPw==", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@material/layout-grid": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-veDABLxMn2RmvfnUO2RUmC1OFfWr4cU+MrxKPoDD2hl3l3eDYv5fxws6r5T1JoSyXoaN+oEZpheS0+M9Ure8Pg==", - "dependencies": { - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.6.tgz", + "integrity": "sha512-yF/ih9EJJZc72psFQbwnn8mExIWfTnzWJg+N02hnpXtDPETYLmQswIMBn7+V88lfCaFrMozJsUvcEQIkEPU0Gg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@material/line-ripple": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-f60hVJhIU6I3/17Tqqzch1emUKEcfVVgHVqADbU14JD+oEIz429ZX9ksZ3VChoU3+eejFl+jVdZMLE/LrAuwpg==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.6.tgz", + "integrity": "sha512-5BbCumsFLbCi586Bb1lTWQFkekdQUw8/t8cy++Uq251cl3hbDIGEwD9HAwh8H6IS2F6QA9KdKmO136LmipRNkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@material/linear-progress": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-pRDEwPQielDiC9Sc5XhCXrGxP8wWOnAO8sQlMebfBYHYqy5hhiIzibezS8CSaW4MFQFyXmCmpmqWlbqGYRmiyg==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.6.tgz", + "integrity": "sha512-+6XgLpMb7HBoWxXj+bLbiiB4s0mRRcDPElnRS3LpWRzdYSe+gFk5MT/4RrVNqd2MESUDmb53NUXw1+BP69bjiQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/list": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-Is0NV91sJlXF5pOebYAtWLF4wU2MJDbYqztML/zQNENkQxDOvEXu3nWNb3YScMIYJJXvARO0Liur5K4yPagS1Q==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.6.tgz", + "integrity": "sha512-l5VmJamJ3nyMmeD1ANBQCQqy7do1ESaJQfKPSm2IG9/ADZryptTyCj8N6QaYgIWewqNUrcbdMkJajRQAt5Qjfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/menu": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-D11QU1dXqLbh5X1zKlEhS3QWh0b5BPNXlafc5MXfkdJHhOiieb7LC9hMJhbrHtj24FadJ7evaFW/T2ugJbJNnQ==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.6.tgz", + "integrity": "sha512-nDYT8qN9si5+onHYYaI4DiauDMx24OAiuZAUsEqrDy+ja/3EbpXPX/VAkMV8AEaQhy3xc4dRC+KcYIvOFefJ4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/menu-surface": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-7RZHvw0gbwppaAJ/Oh5SWmfAKJ62aw1IMB3+3MRwsb5PLoV666wInYa+zJfE4i7qBeOn904xqT2Nko5hY0ssrg==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.6.tgz", + "integrity": "sha512-XlqVtILonQnG+9fH2N3Aytria7P/1fwDgDhl29rde96uH2sLB8CHORIf2PfuLVzFQJ7Uqp8py9AYwr3ZUCFfWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@material/notched-outline": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-Yg2usuKB2DKlKIBISbie9BFsOVuffF71xjbxPbybvqemxqUBd+bD5/t6H1fLE+F8/NCu5JMigho4ewUU+0RCiw==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@material/progress-indicator": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-UPbDjE5CqT+SqTs0mNFG6uFEw7wBlgYmh+noSkQ6ty/EURm8lF125dmi4dv4kW0+octonMXqkGtAoZwLIHKf/w==", - "dependencies": { - "tslib": "^2.1.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@material/radio": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-wR1X0Sr0KmQLu6+YOFKAI84G3L6psqd7Kys5kfb8WKBM36zxO5HQXC5nJm/Y0rdn22ixzsIz2GBo0MNU4V4k1A==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/ripple": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-JqOsWM1f4aGdotP0rh1vZlPZTg6lZgh39FIYHFMfOwfhR+LAikUJ+37ciqZuewgzXB6iiRO6a8aUH6HR5SJYPg==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/rtl": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-UVf14qAtmPiaaZjuJtmN36HETyoKWmsZM/qn1L5ciR2URb8O035dFWnz4ZWFMmAYBno/L7JiZaCkPurv2ZNrGA==", - "dependencies": { - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/segmented-button": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-LCnVRUSAhELTKI/9hSvyvIvQIpPpqF29BV+O9yM4WoNNmNWqTulvuiv7grHZl6Z+kJuxSg4BGbsPxxb9dXozPg==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@material/select": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-WioZtQEXRpglum0cMSzSqocnhsGRr+ZIhvKb3FlaNrTaK8H3Y4QA7rVjv3emRtrLOOjaT6/RiIaUMTo9AGzWQQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu": "15.0.0-canary.7f224ddd4.0", - "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", - "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", + "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.0.1", + "@napi-rs/nice-android-arm64": "1.0.1", + "@napi-rs/nice-darwin-arm64": "1.0.1", + "@napi-rs/nice-darwin-x64": "1.0.1", + "@napi-rs/nice-freebsd-x64": "1.0.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1", + "@napi-rs/nice-linux-arm64-gnu": "1.0.1", + "@napi-rs/nice-linux-arm64-musl": "1.0.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.0.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.0.1", + "@napi-rs/nice-linux-s390x-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-musl": "1.0.1", + "@napi-rs/nice-win32-arm64-msvc": "1.0.1", + "@napi-rs/nice-win32-ia32-msvc": "1.0.1", + "@napi-rs/nice-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz", + "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/shape": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-8z8l1W3+cymObunJoRhwFPKZ+FyECfJ4MJykNiaZq7XJFZkV6xNmqAVrrbQj93FtLsECn9g4PjjIomguVn/OEw==", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz", + "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/slider": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-QU/WSaSWlLKQRqOhJrPgm29wqvvzRusMqwAcrCh1JTrCl+xwJ43q5WLDfjYhubeKtrEEgGu9tekkAiYfMG7EBw==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/snackbar": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-sm7EbVKddaXpT/aXAYBdPoN0k8yeg9+dprgBUkrdqGzWJAeCkxb4fv2B3He88YiCtvkTz2KLY4CThPQBSEsMFQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz", + "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/switch": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-lEDJfRvkVyyeHWIBfoxYjJVl+WlEAE2kZ/+6OqB1FW0OV8ftTODZGhHRSzjVBA1/p4FPuhAtKtoK9jTpa4AZjA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "safevalues": "^0.3.4", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/tab": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-E1xGACImyCLurhnizyOTCgOiVezce4HlBFAI6YhJo/AyVwjN2Dtas4ZLQMvvWWqpyhITNkeYdOchwCC1mrz3AQ==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/tab-bar": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-p1Asb2NzrcECvAQU3b2SYrpyJGyJLQWR+nXTYzDKE8WOpLIRCXap2audNqD7fvN/A20UJ1J8U01ptrvCkwJ4eA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/tab": "15.0.0-canary.7f224ddd4.0", - "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/tab-indicator": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-h9Td3MPqbs33spcPS7ecByRHraYgU4tNCZpZzZXw31RypjKvISDv/PS5wcA4RmWqNGih78T7xg4QIGsZg4Pk4w==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/tab-scroller": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-LFeYNjQpdXecwECd8UaqHYbhscDCwhGln5Yh+3ctvcEgvmDPNjhKn/DL3sWprWvG8NAhP6sHMrsGhQFVdCWtTg==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/tab": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/textfield": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-AExmFvgE5nNF0UA4l2cSzPghtxSUQeeoyRjFLHLy+oAaE4eKZFrSy0zEpqPeWPQpEMDZk+6Y+6T3cOFYBeSvsw==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", - "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz", + "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/theme": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-hs45hJoE9yVnoVOcsN1jklyOa51U4lzWsEnQEuJTPOk2+0HqCQ0yv/q0InpSnm2i69fNSyZC60+8HADZGF8ugQ==", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/tokens": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-r9TDoicmcT7FhUXC4eYMFnt9TZsz0G8T3wXvkKncLppYvZ517gPyD/1+yhuGfGOxAzxTrM66S/oEc1fFE2q4hw==", - "dependencies": { - "@material/elevation": "15.0.0-canary.7f224ddd4.0" + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/tooltip": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-8qNk3pmPLTnam3XYC1sZuplQXW9xLn4Z4MI3D+U17Q7pfNZfoOugGr+d2cLA9yWAEjVJYB0mj8Yu86+udo4N9w==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "safevalues": "^0.3.4", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/top-app-bar": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-SARR5/ClYT4CLe9qAXakbr0i0cMY0V3V4pe3ElIJPfL2Z2c4wGR1mTR8m2LxU1MfGKK8aRoUdtfKaxWejp+eNA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/touch-target": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-BJo/wFKHPYLGsRaIpd7vsQwKr02LtO2e89Psv0on/p0OephlNIgeB9dD9W+bQmaeZsZ6liKSKRl6wJWDiK71PA==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz", + "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/typography": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-kBaZeCGD50iq1DeRRH5OM5Jl7Gdk+/NOfKArkY4ksBZvJiStJ7ACAhpvb8MEGm4s3jvDInQFLsDq3hL+SA79sQ==", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, "node_modules/@ngrx/component-store": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/component-store/-/component-store-17.2.0.tgz", - "integrity": "sha512-ywhyoZpkbVIY1t5zf7xfWLGkY0A/fQdMjPehHloDI6bRLrmbllBhQRazwZ+FAGIi2myx1+mGcmAc6FbtIikedA==", + "version": "19.0.0-beta.0", + "resolved": "https://registry.npmjs.org/@ngrx/component-store/-/component-store-19.0.0-beta.0.tgz", + "integrity": "sha512-yJYxRR28SazEj8xBfKWznvnblWsWt9gY5yqEgTatFc1BiA3dptMKSPrHf6WuXtsqRDg/UnM/oEoywM0LWz05Wg==", + "license": "MIT", "dependencies": { - "@ngrx/operators": "17.0.0-beta.0", "tslib": "^2.0.0" }, "peerDependencies": { - "@angular/core": "^17.0.0", + "@angular/core": "^19.0.0", "rxjs": "^6.5.3 || ^7.5.0" } }, "node_modules/@ngrx/effects": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-17.2.0.tgz", - "integrity": "sha512-tXDJNsuBtbvI/7+vYnkDKKpUvLbopw1U5G6LoPnKNrbTPsPcUGmCqF5Su/ZoRN3BhXjt2j+eoeVdpBkxdxMRgg==", + "version": "19.0.0-beta.0", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-19.0.0-beta.0.tgz", + "integrity": "sha512-No+qZtPxOGCp07Ob06qJKwzLFd/edlKnyX0eQlP8eP9OCNkFJYKgXRbCTBzHWHwq1heGxV/prMEm3YTs3bJWAQ==", + "license": "MIT", "dependencies": { - "@ngrx/operators": "17.0.0-beta.0", "tslib": "^2.0.0" }, "peerDependencies": { - "@angular/core": "^17.0.0", - "@ngrx/store": "17.2.0", - "rxjs": "^6.5.3 || ^7.5.0" + "@angular/core": "^19.0.0", + "@ngrx/store": "19.0.0-beta.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/operators": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-19.1.0.tgz", + "integrity": "sha512-gpo69FnoAF69X68pk9eWFHB630xqerBYkF68wOFMciLOV2im3b/fAf+0sRvnQJmVtG/8jO0IPVdvLqa1TbWmPA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@ngrx/operators": { - "version": "17.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-17.0.0-beta.0.tgz", - "integrity": "sha512-EbO8AONuQ6zo2v/mPyBOi4y0CTAp1x4Z+bx7ZF+Pd8BL5ma53BTCL1TmzaeK5zPUe0yApudLk9/ZbHXPnVox5A==", + "node_modules/@ngrx/signals": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/signals/-/signals-19.1.0.tgz", + "integrity": "sha512-v8sbb+Iox9kdIaKbFgt4Z1W+NxzIU4+g+6qQU6/c27UmtQXv0s1zUKKofPRK0qwkaZzNWkxNToxyoE285ukqcQ==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { + "@angular/core": "^19.0.0", "rxjs": "^6.5.3 || ^7.4.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } } }, "node_modules/@ngrx/store": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-17.2.0.tgz", - "integrity": "sha512-7wKgZ59B/6yQSvvsU0DQXipDqpkAXv7LwcXLD5Ww7nvqN0fQoRPThMh4+Wv55DCJhE0bQc1NEMciLA47uRt7Wg==", + "version": "19.0.0-beta.0", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-19.0.0-beta.0.tgz", + "integrity": "sha512-vc7WgocKsQAcz7w5JFIwSSQauIl5OmDm92Wq00Vc2PjMWM2gcns20IHK+qAlpK/scJ6oiiBztWK3kNTu24QaEg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { - "@angular/core": "^17.0.0", + "@angular/core": "^19.0.0", "rxjs": "^6.5.3 || ^7.5.0" } }, "node_modules/@ngtools/webpack": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.6.tgz", - "integrity": "sha512-equxbgh2DKzZtiFMoVf1KD4yJcH1q8lpqQ/GSPPQUvONcmHrr+yqdRUdaJ7oZCyCYmXF/nByBxtMKtJr6nKZVg==", + "version": "19.2.9", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.9.tgz", + "integrity": "sha512-CLfUauqi2Xp/jKGxp5wUwjqfVQWcBE09GMd51ovcCRLkgB2Kh26+CiVnGw5/lkBpISUCNdgN6nGiS+nfqMfFeQ==", "dev": true, + "license": "MIT", "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "peerDependencies": { - "@angular/compiler-cli": "^17.0.0", - "typescript": ">=5.2 <5.5", + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "typescript": ">=5.5 <5.9", "webpack": "^5.54.0" } }, @@ -4336,6 +4543,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -4349,6 +4557,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -4358,6 +4567,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -4367,10 +4577,11 @@ } }, "node_modules/@npmcli/agent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", - "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", "dev": true, + "license": "ISC", "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", @@ -4379,47 +4590,47 @@ "socks-proxy-agent": "^8.0.3" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "license": "ISC" }, "node_modules/@npmcli/fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", "dev": true, + "license": "ISC", "dependencies": { "semver": "^7.3.5" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/git": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.7.tgz", - "integrity": "sha512-WaOVvto604d5IpdCRV2KjQu8PzkfE96d50CQGKgywXh2GxXmDeUO5EWcBC4V57uFyrNqx83+MewuJh3WTR3xPA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", + "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", "lru-cache": "^10.0.1", - "npm-pick-manifest": "^9.0.0", - "proc-log": "^4.0.0", - "promise-inflight": "^1.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "semver": "^7.3.5", - "which": "^4.0.0" + "which": "^5.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/git/node_modules/isexe": { @@ -4427,33 +4638,24 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/@npmcli/git/node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "license": "ISC" }, "node_modules/@npmcli/git/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -4461,93 +4663,87 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/installed-package-contents": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", - "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", "dev": true, + "license": "ISC", "dependencies": { - "npm-bundled": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" }, "bin": { "installed-package-contents": "bin/index.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/node-gyp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", - "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", + "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", "dev": true, + "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/package-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.1.0.tgz", - "integrity": "sha512-1aL4TuVrLS9sf8quCLerU3H9J4vtCtgu8VauYozrmEyU57i/EdKleCnsQ7vpnABIH6c9mnTxcH5sFkO3BlV8wQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.1.1.tgz", + "integrity": "sha512-d5qimadRAUCO4A/Txw71VM7UrRZzV+NPclxz/dc+M6B2oYwjWTjqh8HA/sGQgs9VZuJ6I/P7XIAlJvgrl27ZOw==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/git": "^5.0.0", + "@npmcli/git": "^6.0.0", "glob": "^10.2.2", - "hosted-git-info": "^7.0.0", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "proc-log": "^4.0.0", - "semver": "^7.5.3" + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@npmcli/package-json/node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/@npmcli/promise-spawn": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", - "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.2.tgz", + "integrity": "sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ==", "dev": true, + "license": "ISC", "dependencies": { - "which": "^4.0.0" + "which": "^5.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/promise-spawn/node_modules/isexe": { @@ -4555,15 +4751,17 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -4571,32 +4769,35 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/redact": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-1.1.0.tgz", - "integrity": "sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.0.tgz", + "integrity": "sha512-NyJXHoZwJE0iUsCDTclXf1bWHJTsshtnp5xUN6F2vY+OLJv6d2cNc4Do6fKNkmPToB0GzoffxRh405ibTwG+Og==", "dev": true, + "license": "ISC", "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/run-script": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", - "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz", + "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/package-json": "^5.0.0", - "@npmcli/promise-spawn": "^7.0.0", - "node-gyp": "^10.0.0", - "which": "^4.0.0" + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/run-script/node_modules/isexe": { @@ -4604,15 +4805,17 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, "node_modules/@npmcli/run-script/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -4620,508 +4823,684 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "node_modules/@nrwl/devkit": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-17.2.8.tgz", - "integrity": "sha512-l2dFy5LkWqSA45s6pee6CoqJeluH+sjRdVnAAQfjLHRNSx6mFAKblyzq5h1f4P0EUCVVVqLs+kVqmNx5zxYqvw==", - "dev": true, - "dependencies": { - "@nx/devkit": "17.2.8" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/@nrwl/tao": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-17.2.8.tgz", - "integrity": "sha512-Qpk5YKeJ+LppPL/wtoDyNGbJs2MsTi6qyX/RdRrEc8lc4bk6Cw3Oul1qTXCI6jT0KzTz+dZtd0zYD/G7okkzvg==", - "dev": true, - "dependencies": { - "nx": "17.2.8", - "tslib": "^2.3.0" - }, - "bin": { - "tao": "index.js" - } - }, - "node_modules/@nx/devkit": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-17.2.8.tgz", - "integrity": "sha512-6LtiQihtZwqz4hSrtT5cCG5XMCWppG6/B8c1kNksg97JuomELlWyUyVF+sxmeERkcLYFaKPTZytP0L3dmCFXaw==", - "dev": true, - "dependencies": { - "@nrwl/devkit": "17.2.8", - "ejs": "^3.1.7", - "enquirer": "~2.3.6", - "ignore": "^5.0.4", - "semver": "7.5.3", - "tmp": "~0.2.1", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "nx": ">= 16 <= 18" - } - }, - "node_modules/@nx/devkit/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, "dependencies": { - "yallist": "^4.0.0" + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/@nx/devkit/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" + "node": ">= 10.0.0" }, - "bin": { - "semver": "bin/semver.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/devkit/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@nx/nx-darwin-arm64": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-17.2.8.tgz", - "integrity": "sha512-dMb0uxug4hM7tusISAU1TfkDK3ixYmzc1zhHSZwpR7yKJIyKLtUpBTbryt8nyso37AS1yH+dmfh2Fj2WxfBHTg==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-darwin-x64": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-17.2.8.tgz", - "integrity": "sha512-0cXzp1tGr7/6lJel102QiLA4NkaLCkQJj6VzwbwuvmuCDxPbpmbz7HC1tUteijKBtOcdXit1/MEoEU007To8Bw==", + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-freebsd-x64": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-17.2.8.tgz", - "integrity": "sha512-YFMgx5Qpp2btCgvaniDGdu7Ctj56bfFvbbaHQWmOeBPK1krNDp2mqp8HK6ZKOfEuDJGOYAp7HDtCLvdZKvJxzA==", + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-17.2.8.tgz", - "integrity": "sha512-iN2my6MrhLRkVDtdivQHugK8YmR7URo1wU9UDuHQ55z3tEcny7LV3W9NSsY9UYPK/FrxdDfevj0r2hgSSdhnzA==", + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-17.2.8.tgz", - "integrity": "sha512-Iy8BjoW6mOKrSMiTGujUcNdv+xSM1DALTH6y3iLvNDkGbjGK1Re6QNnJAzqcXyDpv32Q4Fc57PmuexyysZxIGg==", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-arm64-musl": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-17.2.8.tgz", - "integrity": "sha512-9wkAxWzknjpzdofL1xjtU6qPFF1PHlvKCZI3hgEYJDo4mQiatGI+7Ttko+lx/ZMP6v4+Umjtgq7+qWrApeKamQ==", + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-x64-gnu": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-17.2.8.tgz", - "integrity": "sha512-sjG1bwGsjLxToasZ3lShildFsF0eyeGu+pOQZIp9+gjFbeIkd19cTlCnHrOV9hoF364GuKSXQyUlwtFYFR4VTQ==", + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-x64-musl": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-17.2.8.tgz", - "integrity": "sha512-QiakXZ1xBCIptmkGEouLHQbcM4klQkcr+kEaz2PlNwy/sW3gH1b/1c0Ed5J1AN9xgQxWspriAONpScYBRgxdhA==", + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-17.2.8.tgz", - "integrity": "sha512-XBWUY/F/GU3vKN9CAxeI15gM4kr3GOBqnzFZzoZC4qJt2hKSSUEWsMgeZtsMgeqEClbi4ZyCCkY7YJgU32WUGA==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-win32-x64-msvc": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-17.2.8.tgz", - "integrity": "sha512-HTqDv+JThlLzbcEm/3f+LbS5/wYQWzb5YDXbP1wi7nlCTihNZOLNqGOkEmwlrR5tAdNHPRpHSmkYg4305W0CtA==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" } }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" } }, "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", + "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/pkgr" } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", + "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", + "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", + "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", + "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", + "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", + "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", + "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", - "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", + "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", + "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", + "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", + "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", - "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", + "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", + "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", - "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", + "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", + "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", + "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", + "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", + "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", - "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", + "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@schematics/angular": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.10.tgz", - "integrity": "sha512-rRBlDMXfVPkW3CqVQxazFqkuJXd0BFnD1zjI9WtDiNt3o2pTHbLzuWJnXKuIt5rzv0x/bFwNqIt4CPW2DYGNMg==", + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.0.7.tgz", + "integrity": "sha512-1WtTqKFPuEaV99VIP+y/gf/XW3TVJh/NbJbbEF4qYpp7qQiJ4ntF4klVZmsJcQzFucZSzlg91QVMPQKev5WZGA==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "17.0.10", - "@angular-devkit/schematics": "17.0.10", - "jsonc-parser": "3.2.0" + "@angular-devkit/core": "19.0.7", + "@angular-devkit/schematics": "19.0.7", + "jsonc-parser": "3.3.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.10.tgz", - "integrity": "sha512-93N6oHnmtRt0hL3AXxvnk47sN1rHndfj+pqI5haEY41AGWzIWv9cSBsqlM0PWltNpo6VivcExZESvbLJ71wqbQ==", + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.0.7.tgz", + "integrity": "sha512-VyuORSitT6LIaGUEF0KEnv2TwNaeWl6L3/4L4stok0BJ23B4joVca2DYVcrLC1hSzz8V4dwVgSlbNIgjgGdVpg==", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "picomatch": "3.0.1", + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "peerDependencies": { - "chokidar": "^3.5.2" + "chokidar": "^4.0.0" }, "peerDependenciesMeta": { "chokidar": { @@ -5129,143 +5508,167 @@ } } }, - "node_modules/@schematics/angular/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true + "node_modules/@schematics/angular/node_modules/@angular-devkit/schematics": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.0.7.tgz", + "integrity": "sha512-BHXQv6kMc9xo4TH9lhwMv8nrZXHkLioQvLun2qYjwvOsyzt3qd+sUM9wpHwbG6t+01+FIQ05iNN9ox+Cvpndgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.0.7", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.12", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } }, - "node_modules/@schematics/angular/node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "node_modules/@schematics/angular/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" } }, "node_modules/@sigstore/bundle": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.1.tgz", - "integrity": "sha512-eqV17lO3EIFqCWK3969Rz+J8MYrRZKw9IBHpSo6DEcEX2c+uzDFOgHE9f2MnyDpfs48LFO4hXmk9KhQ74JzU1g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", + "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.3.1" + "@sigstore/protobuf-specs": "^0.4.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@sigstore/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", - "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", + "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.1.tgz", - "integrity": "sha512-aIL8Z9NsMr3C64jyQzE0XlkEyBLpgEJJFDHLVVStkFV5Q3Il/r/YtY6NJWKQ4cy4AE7spP1IX5Jq7VCAxHHMfQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.1.tgz", + "integrity": "sha512-7MJXQhIm7dWF9zo7rRtMYh8d2gSnc3+JddeQOTIg6gUN7FjcuckZ9EwGq+ReeQtbbl3Tbf5YqRrWxA1DMfIn+w==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@sigstore/sign": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.0.tgz", - "integrity": "sha512-tsAyV6FC3R3pHmKS880IXcDJuiFJiKITO1jxR1qbplcsBkZLBmjrEw5GbC7ikD6f5RU1hr7WnmxB/2kKc1qUWQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz", + "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.3.0", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.1", - "make-fetch-happen": "^13.0.0" + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@sigstore/tuf": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.2.tgz", - "integrity": "sha512-mwbY1VrEGU4CO55t+Kl6I7WZzIl+ysSzEYdA1Nv/FTrl2bkeaPXo5PnWZAVfcY2zSdhOpsUTJW67/M2zHXGn5w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.0.tgz", + "integrity": "sha512-suVMQEA+sKdOz5hwP9qNcEjX6B45R+hFFr4LAWzbRc5O+U2IInwvay/bpG5a4s+qR35P/JK/PiKiRGjfuLy1IA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.3.0", - "tuf-js": "^2.2.0" + "@sigstore/protobuf-specs": "^0.4.0", + "tuf-js": "^3.0.1" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@sigstore/verify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.0.tgz", - "integrity": "sha512-hQF60nc9yab+Csi4AyoAmilGNfpXT+EXdBgFkP9OgPwIBPwyqVf7JAWPtmqrrrneTmAT6ojv7OlH1f6Ix5BG4Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.0.tgz", + "integrity": "sha512-kAAM06ca4CzhvjIZdONAL9+MLppW3K48wOFy1TbuaWFW/OMfl8JuTgW0Bm02JB1WJGT/ET2eqav0KTEKmxqkIA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.3.1", - "@sigstore/core": "^1.1.0", - "@sigstore/protobuf-specs": "^0.3.1" + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", "dev": true, + "license": "MIT", "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@tufjs/models": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", - "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", + "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", "dev": true, + "license": "MIT", "dependencies": { "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" + "minimatch": "^9.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@types/body-parser": { @@ -5273,6 +5676,7 @@ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, + "license": "MIT", "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -5283,6 +5687,7 @@ "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -5292,6 +5697,7 @@ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -5301,31 +5707,28 @@ "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, + "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" } }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", - "dev": true - }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5336,22 +5739,32 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, + "license": "MIT", "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, + "node_modules/@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -5360,10 +5773,24 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", - "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -5375,13 +5802,15 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.14", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", - "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -5390,27 +5819,31 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.6.tgz", "integrity": "sha512-3N0FpQTeiWjm+Oo1WUYWguUS7E6JLceiGTriFrG8k5PU7zRLJCzLcWURU3wjMbZGS//a2/LgjsnO3QxIlwxt9g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { - "version": "20.12.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz", - "integrity": "sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==", + "version": "22.15.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz", + "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.21.0" } }, "node_modules/@types/node-forge": { @@ -5418,39 +5851,38 @@ "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", - "dev": true + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "dev": true - }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, + "license": "MIT", "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -5461,6 +5893,7 @@ "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, + "license": "MIT", "dependencies": { "@types/express": "*" } @@ -5470,6 +5903,7 @@ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -5481,555 +5915,520 @@ "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.0.tgz", + "integrity": "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/type-utils": "8.31.0", + "@typescript-eslint/utils": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", + "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "@types/json-schema": "^7.0.3", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^10.12.0 || >=12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "*" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, + "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^10.12.0 || >=12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "eslint-visitor-keys": "^1.1.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.18.0.tgz", - "integrity": "sha512-ZeMtrXnGmTcHciJN1+u2CigWEEXgy1ufoxtWcHORt5kGvpjjIlK9MUhzHm4RM8iVy6dqSaZA/6PVkX6+r+ChjQ==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/typescript-estree": "6.18.0", - "@typescript-eslint/utils": "6.18.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=8.0.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.0.tgz", - "integrity": "sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=4" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.0.tgz", - "integrity": "sha512-klNvl+Ql4NsBNGB4W9TZ2Od03lm7aGvTbs0wYaFYsplVPhr+oeXjlPZCDI4U9jgJIDK38W1FKhacCFzCC+nbIg==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.18.0", - "@typescript-eslint/visitor-keys": "6.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, + "license": "BSD-2-Clause", "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=4.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz", - "integrity": "sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==", + "node_modules/@typescript-eslint/parser": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.0.tgz", + "integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.18.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", + "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.0.tgz", + "integrity": "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, - "node_modules/@typescript-eslint/utils": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.18.0.tgz", - "integrity": "sha512-wiKKCbUeDPGaYEYQh1S580dGxJ/V9HI7K5sbGAVklyf+o5g3O+adnS4UNJajplF4e7z2q0uVBaTdT/yLb4XAVA==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.0.tgz", + "integrity": "sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.18.0", - "@typescript-eslint/types": "6.18.0", - "@typescript-eslint/typescript-estree": "6.18.0", - "semver": "^7.5.4" + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/utils": "8.31.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.0.tgz", - "integrity": "sha512-o/UoDT2NgOJ2VfHpfr+KBY2ErWvCySNUIX/X7O9g8Zzt/tXdpfEU43qbNk8LVuWUT2E0ptzTWXh79i74PP0twA==", + "node_modules/@typescript-eslint/types": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", + "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.18.0", - "@typescript-eslint/visitor-keys": "6.18.0" - }, + "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.0.tgz", - "integrity": "sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz", + "integrity": "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.0.tgz", - "integrity": "sha512-klNvl+Ql4NsBNGB4W9TZ2Od03lm7aGvTbs0wYaFYsplVPhr+oeXjlPZCDI4U9jgJIDK38W1FKhacCFzCC+nbIg==", + "node_modules/@typescript-eslint/utils": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.0.tgz", + "integrity": "sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.18.0", - "@typescript-eslint/visitor-keys": "6.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz", - "integrity": "sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz", + "integrity": "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.18.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "8.31.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, + "license": "Apache-2.0", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/eslint" } }, "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", - "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", + "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", "dev": true, + "license": "MIT", "engines": { - "node": ">=14.6.0" + "node": ">=14.21.3" }, "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" } }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, + "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -6037,58 +6436,31 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true - }, - "node_modules/@yarnpkg/parsers": { - "version": "3.0.0-rc.46", - "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.46.tgz", - "integrity": "sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==", - "dev": true, - "dependencies": { - "js-yaml": "^3.10.0", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.15.0" - } - }, - "node_modules/@zkochan/js-yaml": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", - "integrity": "sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@zkochan/js-yaml/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", "dev": true, + "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/accepts": { @@ -6096,6 +6468,7 @@ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, + "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -6104,11 +6477,22 @@ "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -6116,20 +6500,12 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -6139,6 +6515,7 @@ "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", "dev": true, + "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "regex-parser": "^2.2.11" @@ -6152,6 +6529,7 @@ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, + "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -6162,40 +6540,26 @@ } }, "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, + "license": "MIT", "engines": { "node": ">= 14" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -6203,10 +6567,11 @@ } }, "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -6224,6 +6589,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -6236,6 +6602,7 @@ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6245,6 +6612,7 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -6263,29 +6631,35 @@ "engines": [ "node >= 0.8.0" ], + "license": "Apache-2.0", "bin": { "ansi-html": "bin/ansi-html" } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/anymatch": { @@ -6293,6 +6667,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -6306,6 +6681,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -6314,54 +6690,43 @@ } }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } + "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, "node_modules/autoprefixer": { - "version": "10.4.18", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", - "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -6377,12 +6742,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001591", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -6395,31 +6761,22 @@ "postcss": "^8.1.0" } }, - "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/axobject-query": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", - "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/babel-loader": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", - "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", "dev": true, + "license": "MIT", "dependencies": { "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" @@ -6432,30 +6789,15 @@ "webpack": ">=5" } }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", - "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", + "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", "dev": true, + "license": "MIT", "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" }, "peerDependencies": { @@ -6467,62 +6809,33 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", - "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0", - "core-js-compat": "^3.34.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", - "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", + "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "@babel/helper-define-polyfill-provider": "^0.6.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -6532,13 +6845,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -6552,13 +6864,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true, + "license": "MIT", "engines": { "node": "^4.5.0 || >= 5.9" } @@ -6567,13 +6881,35 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/beasties": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.2.tgz", + "integrity": "sha512-p4AF8uYzm9Fwu8m/hSVTCPXrRBPmB34hQpHsec2KOaR9CZmgoU8IOv4Cvwq4hgz2p4hLMNbsdNl5XeA6XbAQwA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -6583,6 +6919,7 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -6594,7 +6931,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, + "license": "MIT", "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -6602,10 +6939,11 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, + "license": "MIT", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -6615,7 +6953,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -6630,6 +6968,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -6638,13 +6977,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/bonjour-service": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", - "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" @@ -6654,13 +6995,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -6670,6 +7013,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -6678,9 +7022,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -6696,11 +7040,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -6713,7 +7058,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -6728,6 +7072,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -6737,24 +7082,42 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/cacache": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.3.tgz", - "integrity": "sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==", + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/fs": "^3.1.0", + "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", @@ -6762,57 +7125,120 @@ "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" } }, "node_modules/cacache/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "node_modules/cacache/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, "engines": { - "node": "14 || >=16.14" + "node": ">=18" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, + "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -6826,23 +7252,15 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001616", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz", - "integrity": "sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==", + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", "dev": true, "funding": [ { @@ -6857,50 +7275,47 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chownr": { @@ -6908,45 +7323,43 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { - "restore-cursor": "^3.1.0" + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-spinners": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -6954,11 +7367,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, + "license": "ISC", "engines": { "node": ">= 12" } @@ -6968,6 +7399,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -6977,44 +7409,44 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/cliui/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=8" } }, - "node_modules/cliui/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -7032,6 +7464,7 @@ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8" } @@ -7041,6 +7474,7 @@ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, + "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", @@ -7050,56 +7484,76 @@ "node": ">=6" } }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } + "license": "MIT" }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "license": "MIT", + "dependencies": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } }, "node_modules/common-path-prefix": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -7109,6 +7563,7 @@ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -7117,37 +7572,30 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", "dev": true, + "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", + "negotiator": "~0.6.4", "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -7156,25 +7604,46 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } }, "node_modules/connect": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", @@ -7190,6 +7659,7 @@ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8" } @@ -7199,6 +7669,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -7207,13 +7678,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -7226,6 +7699,7 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -7234,13 +7708,15 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -7249,13 +7725,15 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/copy-anything": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", "dev": true, + "license": "MIT", "dependencies": { "is-what": "^3.14.1" }, @@ -7264,20 +7742,21 @@ } }, "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", "dev": true, + "license": "MIT", "dependencies": { - "fast-glob": "^3.2.11", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.1", - "globby": "^13.1.1", + "globby": "^14.0.0", "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -7287,56 +7766,14 @@ "webpack": "^5.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", - "dev": true, - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", - "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/core-js-compat": { - "version": "3.37.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.0.tgz", - "integrity": "sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==", + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", + "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", "dev": true, + "license": "MIT", "dependencies": { - "browserslist": "^4.23.0" + "browserslist": "^4.24.4" }, "funding": { "type": "opencollective", @@ -7347,13 +7784,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, + "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -7367,6 +7806,7 @@ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, + "license": "MIT", "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -7388,114 +7828,12 @@ } } }, - "node_modules/cosmiconfig/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/critters": { - "version": "0.0.22", - "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.22.tgz", - "integrity": "sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "css-select": "^5.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.2", - "htmlparser2": "^8.0.2", - "postcss": "^8.4.23", - "postcss-media-query-parser": "^0.2.3" - } - }, - "node_modules/critters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/critters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/critters/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/critters/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/critters/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/critters/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -7506,22 +7844,23 @@ } }, "node_modules/css-loader": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", - "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", "dev": true, + "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.4", - "postcss-modules-scope": "^3.1.1", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.5.4" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -7529,7 +7868,7 @@ }, "peerDependencies": { "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" + "webpack": "^5.27.0" }, "peerDependenciesMeta": { "@rspack/core": { @@ -7545,6 +7884,7 @@ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -7561,6 +7901,7 @@ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -7573,6 +7914,7 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -7584,24 +7926,26 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0" } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -7616,18 +7960,37 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", "dev": true, - "dependencies": { - "execa": "^5.0.0" - }, + "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/defaults": { @@ -7635,6 +7998,7 @@ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "dev": true, + "license": "MIT", "dependencies": { "clone": "^1.0.2" }, @@ -7642,39 +8006,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/depd": { @@ -7682,73 +8024,60 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", - "dev": true - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, + "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, @@ -7761,6 +8090,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -7773,6 +8103,7 @@ "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", "dev": true, + "license": "MIT", "dependencies": { "custom-event": "~1.0.0", "ent": "~2.2.0", @@ -7785,6 +8116,7 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -7804,13 +8136,15 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -7822,10 +8156,11 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -7835,77 +8170,67 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/dotenv": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.2.tgz", - "integrity": "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" - } - }, - "node_modules/dotenv-expand": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", - "dev": true, "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.4.757", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.757.tgz", - "integrity": "sha512-jftDaCknYSSt/+KKeXzH3LX5E2CvRLm75P3Hj+J/dv3CL0qUYcOt13d5FN1NiL5IJbbhzHrb3BomeG2tkSlZmw==", - "dev": true + "version": "1.5.142", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.142.tgz", + "integrity": "sha512-Ah2HgkTu/9RhTDNThBtzu2Wirdy4DC9b0sMT1pUhbkZQ5U/iwmE+PHZX1MpjD5IkJCc2wSghgGG/B04szAx07w==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -7915,6 +8240,7 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -7924,6 +8250,7 @@ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -7934,6 +8261,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7946,23 +8274,23 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, + "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", "dev": true, + "license": "MIT", "dependencies": { - "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -7973,19 +8301,61 @@ } }, "node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -7995,28 +8365,41 @@ } }, "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.1" + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8.6" } }, "node_modules/ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", - "dev": true + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "devOptional": true, + "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -8029,21 +8412,37 @@ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "prr": "~1.0.1" @@ -8057,18 +8456,17 @@ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -8078,72 +8476,91 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-module-lexer": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.2.tgz", - "integrity": "sha512-l60ETUTmLqbVbVHv1J4/qj+M8nq7AwMzEcg3kmJDt9dCNrTk+yHcYFf/Kw75pMDwd9mPcIGCG5LcS20SxYRzFA==", - "dev": true + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/esbuild": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", - "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "dev": true, "hasInstallScript": true, - "optional": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.1", - "@esbuild/android-arm": "0.20.1", - "@esbuild/android-arm64": "0.20.1", - "@esbuild/android-x64": "0.20.1", - "@esbuild/darwin-arm64": "0.20.1", - "@esbuild/darwin-x64": "0.20.1", - "@esbuild/freebsd-arm64": "0.20.1", - "@esbuild/freebsd-x64": "0.20.1", - "@esbuild/linux-arm": "0.20.1", - "@esbuild/linux-arm64": "0.20.1", - "@esbuild/linux-ia32": "0.20.1", - "@esbuild/linux-loong64": "0.20.1", - "@esbuild/linux-mips64el": "0.20.1", - "@esbuild/linux-ppc64": "0.20.1", - "@esbuild/linux-riscv64": "0.20.1", - "@esbuild/linux-s390x": "0.20.1", - "@esbuild/linux-x64": "0.20.1", - "@esbuild/netbsd-x64": "0.20.1", - "@esbuild/openbsd-x64": "0.20.1", - "@esbuild/sunos-x64": "0.20.1", - "@esbuild/win32-arm64": "0.20.1", - "@esbuild/win32-ia32": "0.20.1", - "@esbuild/win32-x64": "0.20.1" + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" } }, "node_modules/esbuild-wasm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz", - "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.1.tgz", + "integrity": "sha512-dZxPeDHcDIQ6ilml/NzYxnPbNkoVsHSFH3JGLSobttc5qYYgExMo8lh2XcB+w+AfiqykVDGK5PWanGB0gWaAWw==", "dev": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -8152,28 +8569,35 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -8223,6 +8647,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8231,13 +8656,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz", + "integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==", "dev": true, + "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.11.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -8248,7 +8674,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -8261,10 +8687,11 @@ } }, "node_modules/eslint-scope": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", - "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -8276,109 +8703,71 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "eslint-visitor-keys": "^1.1.0" }, "engines": { - "node": ">=8" + "node": ">=6" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "license": "Apache-2.0", + "engines": { + "node": ">=4" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=10" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "engines": { - "node": ">=7.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/eslint/node_modules/eslint-scope": { @@ -8386,6 +8775,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -8397,39 +8787,12 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/eslint/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -8440,53 +8803,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "node": ">= 4" } }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -8494,53 +8833,12 @@ "node": "*" } }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -8553,6 +8851,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -8570,6 +8869,7 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, + "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -8579,10 +8879,11 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -8595,6 +8896,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -8607,6 +8909,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -8616,6 +8919,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -8625,6 +8929,7 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -8633,78 +8938,59 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.x" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "node_modules/exponential-backoff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", - "dev": true + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -8713,13 +8999,18 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -8729,18 +9020,30 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -8755,13 +9058,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/express/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -8770,13 +9075,15 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "dev": true, + "license": "MIT", "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", @@ -8786,63 +9093,87 @@ "node": ">=4" } }, - "node_modules/external-editor/node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-diff": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "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.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -8852,6 +9183,7 @@ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dev": true, + "license": "Apache-2.0", "dependencies": { "websocket-driver": ">=0.5.1" }, @@ -8859,19 +9191,19 @@ "node": ">=0.8.0" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/file-entry-cache": { @@ -8879,6 +9211,7 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -8886,32 +9219,12 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -8924,6 +9237,7 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -8942,6 +9256,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -8950,13 +9265,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/finalhandler/node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "dev": true, + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -8969,6 +9286,7 @@ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", "dev": true, + "license": "MIT", "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" @@ -8981,16 +9299,20 @@ } }, "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat": { @@ -8998,6 +9320,7 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, + "license": "BSD-3-Clause", "bin": { "flat": "cli.js" } @@ -9007,6 +9330,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -9017,15 +9341,16 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "dev": true, "funding": [ { @@ -9033,6 +9358,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -9043,45 +9369,20 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, + "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/forwarded": { @@ -9089,6 +9390,7 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -9098,6 +9400,7 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, + "license": "MIT", "engines": { "node": "*" }, @@ -9111,28 +9414,24 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, "engines": { - "node": ">=14.14" + "node": ">=6 <7 || >=8" } }, "node_modules/fs-minipass": { @@ -9140,6 +9439,7 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -9147,17 +9447,11 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/fs-monkey": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", - "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", - "dev": true - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -9165,6 +9459,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -9178,15 +9473,24 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true, + "license": "MIT" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -9196,21 +9500,41 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "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" }, "engines": { "node": ">= 0.4" @@ -9219,32 +9543,26 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.4" } }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -9261,28 +9579,30 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9292,7 +9612,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -9305,37 +9625,50 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", "dev": true, + "license": "MIT", "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" + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9345,25 +9678,29 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^2.0.0" }, @@ -9376,36 +9713,27 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -9413,11 +9741,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -9430,6 +9762,7 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -9437,32 +9770,42 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-3.0.0.tgz", + "integrity": "sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.6", + "readable-stream": "^3.6.0" + } + }, "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^10.0.1" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "license": "ISC" }, "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", @@ -9475,6 +9818,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9489,43 +9833,30 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/hpack.js/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, - "node_modules/html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ] - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -9534,30 +9865,47 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -9574,21 +9922,24 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", - "dev": true + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, + "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -9603,6 +9954,7 @@ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -9612,49 +9964,45 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", + "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/http-proxy": "^1.17.8", + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" }, "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { "node": ">= 14" } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10.17.0" + "node": ">=10.18" } }, "node_modules/iconv-lite": { @@ -9662,6 +10010,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -9674,6 +10023,7 @@ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true, + "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -9685,7 +10035,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -9699,27 +10048,30 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/ignore-walk": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", - "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", + "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==", "dev": true, + "license": "ISC", "dependencies": { "minimatch": "^9.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/image-size": { @@ -9727,6 +10079,7 @@ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", "dev": true, + "license": "MIT", "optional": true, "bin": { "image-size": "bin/image-size.js" @@ -9736,16 +10089,18 @@ } }, "node_modules/immutable": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", - "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", - "dev": true + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", + "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", + "dev": true, + "license": "MIT" }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -9757,20 +10112,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -9780,6 +10127,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -9788,7 +10136,8 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -9798,53 +10147,16 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "license": "ISC" }, "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/inquirer": { - "version": "9.2.15", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", - "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", - "dev": true, - "dependencies": { - "@ljharb/through": "^2.3.12", - "ansi-escapes": "^4.3.2", - "chalk": "^5.3.0", - "cli-cursor": "^3.1.0", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "figures": "^3.2.0", - "lodash": "^4.17.21", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", "dev": true, + "license": "ISC", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/ip-address": { @@ -9852,6 +10164,7 @@ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dev": true, + "license": "MIT", "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" @@ -9860,17 +10173,12 @@ "node": ">= 12" } }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true - }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10" } @@ -9879,13 +10187,15 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -9894,27 +10204,32 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, + "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9925,17 +10240,22 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-glob": { @@ -9943,6 +10263,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -9950,26 +10271,54 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -9979,6 +10328,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -9988,6 +10338,7 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9996,27 +10347,32 @@ } }, "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-unicode-supported": { @@ -10024,6 +10380,7 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -10035,31 +10392,38 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, + "license": "MIT", "dependencies": { - "is-docker": "^2.0.0" + "is-inside-container": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isbinaryfile": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8.0.0" }, @@ -10071,13 +10435,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -10087,33 +10453,26 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" + "semver": "^7.5.4" }, "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "node": ">=10" } }, "node_modules/istanbul-lib-report": { @@ -10121,6 +10480,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -10130,32 +10490,12 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -10170,6 +10510,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -10179,6 +10520,7 @@ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -10188,16 +10530,14 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -10205,221 +10545,19 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jake": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", - "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", - "dev": true, - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jake/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/jake/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jake/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jake/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jake/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jake/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/jake/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jasmine-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", - "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", - "dev": true - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } + "license": "MIT" }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -10429,20 +10567,12 @@ "node": ">= 10.13.0" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -10454,28 +10584,40 @@ } }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, + "license": "MIT", "bin": { "jiti": "bin/jiti.js" } }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -10485,52 +10627,59 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", - "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", "dev": true, + "license": "MIT", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -10539,19 +10688,18 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" }, "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, + "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -10563,13 +10711,15 @@ "dev": true, "engines": [ "node >= 0.2.0" - ] + ], + "license": "MIT" }, "node_modules/karma": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", - "integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, + "license": "MIT", "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -10608,6 +10758,7 @@ "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", "dev": true, + "license": "MIT", "dependencies": { "which": "^1.2.1" } @@ -10617,6 +10768,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -10629,6 +10781,7 @@ "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", "dev": true, + "license": "MIT", "dependencies": { "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-instrument": "^5.1.0", @@ -10646,16 +10799,35 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/karma-coverage/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -10663,11 +10835,22 @@ "node": "*" } }, + "node_modules/karma-coverage/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/karma-jasmine": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", "dev": true, + "license": "MIT", "dependencies": { "jasmine-core": "^4.1.0" }, @@ -10683,6 +10866,7 @@ "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", "dev": true, + "license": "MIT", "peerDependencies": { "jasmine-core": "^4.0.0 || ^5.0.0", "karma": "^6.0.0", @@ -10694,69 +10878,95 @@ "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", "dev": true, + "license": "MIT", "dependencies": { "source-map-support": "^0.5.5" } }, - "node_modules/karma/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/karma/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, + "node_modules/karma/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/karma/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, - "node_modules/karma/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/karma/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">= 6" } }, - "node_modules/karma/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "node_modules/karma/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/karma/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -10764,20 +10974,73 @@ "node": "*" } }, + "node_modules/karma/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/karma/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/karma/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/karma/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/karma/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -10795,6 +11058,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -10813,6 +11077,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -10822,6 +11087,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -10831,34 +11097,28 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/launch-editor": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", - "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", + "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", "dev": true, + "license": "MIT", "dependencies": { "picocolors": "^1.0.0", "shell-quote": "^1.8.1" } }, "node_modules/less": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", - "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", + "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -10881,23 +11141,30 @@ } }, "node_modules/less-loader": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", - "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", "dev": true, - "dependencies": { - "klona": "^2.0.4" - }, + "license": "MIT", "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "less": "^3.5.0 || ^4.0.0", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/less/node_modules/make-dir": { @@ -10905,6 +11172,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "pify": "^4.0.1", @@ -10919,6 +11187,7 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "license": "MIT", "optional": true, "bin": { "mime": "cli.js" @@ -10932,6 +11201,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "optional": true, "bin": { "semver": "bin/semver" @@ -10942,16 +11212,27 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -10965,6 +11246,7 @@ "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", "dev": true, + "license": "ISC", "dependencies": { "webpack-sources": "^3.0.0" }, @@ -10978,12 +11260,122 @@ } }, "node_modules/lines-and-columns": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", - "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.6.tgz", + "integrity": "sha512-SuHqzPl7mYStna8WRotY8XX/EUZBjjv3QyKIByeCLFfC9uXT/OIHByEcA07PzbMfQAM0KYJtLgtpMRlIe5dErQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.2.6", + "@lmdb/lmdb-darwin-x64": "3.2.6", + "@lmdb/lmdb-linux-arm": "3.2.6", + "@lmdb/lmdb-linux-arm64": "3.2.6", + "@lmdb/lmdb-linux-x64": "3.2.6", + "@lmdb/lmdb-win32-x64": "3.2.6" } }, "node_modules/loader-runner": { @@ -10991,133 +11383,209 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.11.5" } }, "node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12.13.0" } }, "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "environment": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "get-east-asian-width": "^1.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/log-symbols/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/log4js": { @@ -11125,6 +11593,7 @@ "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", "dev": true, + "license": "Apache-2.0", "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", @@ -11137,10 +11606,11 @@ } }, "node_modules/loglevel": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", - "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" }, @@ -11154,6 +11624,7 @@ "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^1.1.3", "loglevel": "^1.4.1" @@ -11164,6 +11635,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -11173,6 +11645,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -11182,6 +11655,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", @@ -11193,11 +11667,22 @@ "node": ">=0.10.0" } }, + "node_modules/loglevel-colored-level-prefix/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/loglevel-colored-level-prefix/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^2.0.0" }, @@ -11210,6 +11695,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -11219,20 +11705,19 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } }, "node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/make-dir": { @@ -11240,6 +11725,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -11251,35 +11737,36 @@ } }, "node_modules/make-fetch-happen": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", - "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", "dev": true, + "license": "ISC", "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", + "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", "promise-retry": "^2.0.1", - "ssri": "^10.0.0" + "ssri": "^12.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/make-fetch-happen/node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, + "license": "MIT", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">= 0.4" } }, "node_modules/media-typer": { @@ -11287,39 +11774,54 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "fs-monkey": "^1.0.4" + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" }, "engines": { "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -11329,17 +11831,19 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -11351,6 +11855,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -11363,6 +11868,7 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -11375,6 +11881,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -11384,6 +11891,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -11396,15 +11904,30 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mini-css-extract-plugin": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", - "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", "dev": true, + "license": "MIT", "dependencies": { "schema-utils": "^4.0.0", "tapable": "^2.2.1" @@ -11424,13 +11947,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -11445,16 +11970,17 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.0.tgz", - "integrity": "sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -11464,6 +11990,7 @@ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -11472,17 +11999,18 @@ } }, "node_modules/minipass-fetch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", - "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", "dev": true, + "license": "MIT", "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" + "minizlib": "^3.0.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" }, "optionalDependencies": { "encoding": "^0.1.13" @@ -11493,6 +12021,7 @@ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -11505,6 +12034,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -11516,41 +12046,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", - "dev": true, - "dependencies": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - } - }, - "node_modules/minipass-json-stream/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-json-stream/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "license": "ISC" }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -11563,6 +12067,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -11574,13 +12079,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/minipass-sized": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -11593,6 +12100,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -11604,44 +12112,28 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } + "license": "ISC" }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", "dev": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">= 18" } }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -11649,26 +12141,133 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mqtt": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz", + "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==", + "license": "MIT", + "dependencies": { + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "lru-cache": "^6.0.0", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.9", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^3.1.0", + "ws": "^7.5.5", + "xtend": "^4.0.2" + }, + "bin": { + "mqtt": "bin/mqtt.js", + "mqtt_pub": "bin/pub.js", + "mqtt_sub": "bin/sub.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mqtt-browser": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/mqtt-browser/-/mqtt-browser-4.3.7.tgz", + "integrity": "sha512-4pxHxa3avIILr2CXhTKlArVpATqfyTu4zr5u2PoUwzgw0GDr5dpzZ0pmPgZyOoQBVgrVDEboCzb/b1Q0yWOm7g==", + "license": "MIT", + "dependencies": { + "mqtt": "4.3.7" + } + }, + "node_modules/mqtt-packet": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz", + "integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mqtt/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dev": true, + "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" @@ -11678,18 +12277,19 @@ } }, "node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, + "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -11697,6 +12297,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -11708,13 +12309,15 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/needle": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "iconv-lite": "^0.6.3", @@ -11732,6 +12335,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -11741,10 +12345,11 @@ } }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -11753,12 +12358,14 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ngx-mask": { "version": "16.4.2", "resolved": "https://registry.npmjs.org/ngx-mask/-/ngx-mask-16.4.2.tgz", "integrity": "sha512-mQjcsTpctGu6HYKLf6/gjEUvW65D+46xvPIMYz0BDZXqHXrqKVluHXR3KF++TNOfdLLXwW6SvuHWd91NZN/C1A==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -11768,26 +12375,27 @@ "@angular/forms": ">=14.0.0" } }, - "node_modules/nice-napi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", - "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "!win32" - ], + "node_modules/ngx-mqtt": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/ngx-mqtt/-/ngx-mqtt-17.0.0.tgz", + "integrity": "sha512-54wVMyDOZkpTZEs0rTMWPP1Yz+6q3rRnHzIBnpqnBkDcyMfNrti45C7ijwnEIaPDzQHMOqVrDgh/6C4ocPPLJQ==", + "license": "MIT", "dependencies": { - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.2" + "mqtt-browser": "4.3.7", + "mqtt-packet": "^6.10.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=14", + "@angular/core": ">=14" } }, "node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "dev": true, + "license": "MIT", "optional": true }, "node_modules/node-forge": { @@ -11795,66 +12403,60 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/node-gyp": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", - "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.2.0.tgz", + "integrity": "sha512-T0S1zqskVUSxcsSTkAsLc7xCycrRYmtDHadDinzocrThjyQCn5kMlEBSj6H4qDbgsIOSLmmlRIeb0lZXj+UArA==", "dev": true, + "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^13.0.0", - "nopt": "^7.0.0", - "proc-log": "^3.0.0", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^4.0.0" + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/node-gyp-build": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", - "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", "dev": true, + "license": "MIT", "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, "node_modules/node-gyp/node_modules/isexe": { @@ -11862,15 +12464,51 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, + "node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/node-gyp/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -11878,49 +12516,40 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/node-machine-id": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", - "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", - "dev": true + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" }, "node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", "dev": true, + "license": "ISC", "dependencies": { - "abbrev": "^2.0.0" + "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", - "integrity": "sha512-6rvCfeRW+OEZagAB4lMLSNuTNYZWLVtKccK79VSTf//yTY5VOCgcpH80O+bZK8Neps7pUnd5G+QlMg1yV/2iZQ==", - "dev": true, - "dependencies": { - "hosted-git-info": "^7.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/normalize-path": { @@ -11928,6 +12557,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -11937,1189 +12567,1426 @@ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/npm-bundled": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", - "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", "dev": true, + "license": "ISC", "dependencies": { - "npm-normalize-package-bin": "^3.0.0" + "npm-normalize-package-bin": "^4.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm-install-checks": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", - "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.1.tgz", + "integrity": "sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", "dev": true, + "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm-package-arg": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz", - "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.0.tgz", + "integrity": "sha512-ZTE0hbwSdTNL+Stx2zxSqdu2KZfNDcrtrLdIk7XGnQFYBWYDho/ORvXtn5XEePcL3tFpGjHCV3X3xrtDh7eZ+A==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-packlist": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz", + "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", + "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", + "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, + "license": "MIT", "dependencies": { - "hosted-git-info": "^7.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/npm-packlist": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", - "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "node_modules/ora/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, - "dependencies": { - "ignore-walk": "^6.0.4" - }, + "license": "ISC" + }, + "node_modules/ordered-binary": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", + "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=0.10.0" } }, - "node_modules/npm-pick-manifest": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", - "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^11.0.0", - "semver": "^7.3.5" + "yocto-queue": "^0.1.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm-registry-fetch": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.2.1.tgz", - "integrity": "sha512-8l+7jxhim55S85fjiDGJ1rZXBWGtRLi1OSb4Z3BPLObPuIaeKRlPRiYMSHU4/81ck3t71Z+UwDDl47gcpmfQQA==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { - "@npmcli/redact": "^1.1.0", - "make-fetch-happen": "^13.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^11.0.0", - "proc-log": "^4.0.0" + "p-limit": "^3.0.2" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm-registry-fetch/node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", "dev": true, + "license": "MIT", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", "dev": true, + "license": "MIT", "dependencies": { - "path-key": "^3.0.0" + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" }, "engines": { - "node": ">=8" + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "license": "MIT", + "engines": { + "node": ">= 4" } }, - "node_modules/nx": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/nx/-/nx-17.2.8.tgz", - "integrity": "sha512-rM5zXbuXLEuqQqcjVjClyvHwRJwt+NVImR2A6KFNG40Z60HP6X12wAxxeLHF5kXXTDRU0PFhf/yACibrpbPrAw==", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, - "hasInstallScript": true, - "dependencies": { - "@nrwl/tao": "17.2.8", - "@yarnpkg/lockfile": "^1.1.0", - "@yarnpkg/parsers": "3.0.0-rc.46", - "@zkochan/js-yaml": "0.0.6", - "axios": "^1.5.1", - "chalk": "^4.1.0", - "cli-cursor": "3.1.0", - "cli-spinners": "2.6.1", - "cliui": "^8.0.1", - "dotenv": "~16.3.1", - "dotenv-expand": "~10.0.0", - "enquirer": "~2.3.6", - "figures": "3.2.0", - "flat": "^5.0.2", - "fs-extra": "^11.1.0", - "glob": "7.1.4", - "ignore": "^5.0.4", - "jest-diff": "^29.4.1", - "js-yaml": "4.1.0", - "jsonc-parser": "3.2.0", - "lines-and-columns": "~2.0.3", - "minimatch": "3.0.5", - "node-machine-id": "1.1.12", - "npm-run-path": "^4.0.1", - "open": "^8.4.0", - "semver": "7.5.3", - "string-width": "^4.2.3", - "strong-log-transformer": "^2.1.0", - "tar-stream": "~2.2.0", - "tmp": "~0.2.1", - "tsconfig-paths": "^4.1.2", - "tslib": "^2.3.0", - "yargs": "^17.6.2", - "yargs-parser": "21.1.1" + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-20.0.0.tgz", + "integrity": "sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" }, "bin": { - "nx": "bin/nx.js", - "nx-cloud": "bin/nx-cloud.js" - }, - "optionalDependencies": { - "@nx/nx-darwin-arm64": "17.2.8", - "@nx/nx-darwin-x64": "17.2.8", - "@nx/nx-freebsd-x64": "17.2.8", - "@nx/nx-linux-arm-gnueabihf": "17.2.8", - "@nx/nx-linux-arm64-gnu": "17.2.8", - "@nx/nx-linux-arm64-musl": "17.2.8", - "@nx/nx-linux-x64-gnu": "17.2.8", - "@nx/nx-linux-x64-musl": "17.2.8", - "@nx/nx-win32-arm64-msvc": "17.2.8", - "@nx/nx-win32-x64-msvc": "17.2.8" + "pacote": "bin/index.js" }, - "peerDependencies": { - "@swc-node/register": "^1.6.7", - "@swc/core": "^1.3.85" + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" }, - "peerDependenciesMeta": { - "@swc-node/register": { - "optional": true - }, - "@swc/core": { - "optional": true - } + "engines": { + "node": ">=6" } }, - "node_modules/nx/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { "node": ">=8" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nx/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" }, - "node_modules/nx/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "license": "MIT", + "engines": { + "node": ">= 0.10" } }, - "node_modules/nx/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" + "entities": "^6.0.0" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/nx/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" }, - "engines": { - "node": ">=7.0.0" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/nx/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/nx/node_modules/glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", "dev": true, + "license": "MIT", "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" + "parse5": "^7.0.0" }, - "engines": { - "node": "*" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/nx/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "license": "BSD-2-Clause", "engines": { - "node": ">=8" + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/nx/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "node_modules/nx/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "node_modules/nx/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/nx/node_modules/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", "engines": { - "node": "*" + "node": ">=0.10.0" } }, - "node_modules/nx/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/nx/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "has-flag": "^4.0.0" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/nx/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, + "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, + "license": "MIT", + "optional": true, "engines": { - "node": ">= 0.8" + "node": ">=6" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "node_modules/piscina": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", + "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==", "dev": true, - "dependencies": { - "wrappy": "1" + "license": "MIT", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", "dev": true, + "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "find-up": "^6.3.0" }, "engines": { - "node": ">=6" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", "dev": true, + "license": "MIT", "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" }, "engines": { - "node": ">=12" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", "dev": true, + "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "p-locate": "^6.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", "dev": true, + "license": "MIT", "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" + "yocto-queue": "^1.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "p-limit": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/ora/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, + "license": "MIT", "engines": { - "node": ">=7.0.0" + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/ora/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/postcss": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", + "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14" } }, - "node_modules/ora/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" }, "engines": { - "node": ">=8" + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, + "license": "ISC", "engines": { - "node": ">=0.10.0" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", "dev": true, + "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" }, "engines": { - "node": ">=6" + "node": "^10 || ^12 || >= 14" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dev": true, + "license": "ISC", "dependencies": { - "p-limit": "^2.2.0" + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/p-map": { + "node_modules/postcss-modules-values": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, + "license": "ISC", "dependencies": { - "aggregate-error": "^3.0.0" + "icss-utils": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "^10 || ^12 || >= 14" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, + "license": "MIT", "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/p-retry/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true, - "engines": { - "node": ">= 4" - } + "license": "MIT" }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.8.0" } }, - "node_modules/pacote": { - "version": "17.0.4", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.4.tgz", - "integrity": "sha512-eGdLHrV/g5b5MtD5cTPyss+JxOlaOloSMG3UwPMAvL8ywaLJ6beONPF40K4KKl/UI6q5hTKCJq5rCu8tkF+7Dg==", + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, - "dependencies": { - "@npmcli/git": "^5.0.0", - "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/promise-spawn": "^7.0.0", - "@npmcli/run-script": "^7.0.0", - "cacache": "^18.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^11.0.0", - "npm-packlist": "^8.0.0", - "npm-pick-manifest": "^9.0.0", - "npm-registry-fetch": "^16.0.0", - "proc-log": "^3.0.0", - "promise-retry": "^2.0.1", - "read-package-json": "^7.0.0", - "read-package-json-fast": "^3.0.0", - "sigstore": "^2.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11" - }, + "license": "MIT", "bin": { - "pacote": "lib/bin.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" + "node": ">=14" }, - "engines": { - "node": ">=6" + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "node_modules/prettier-eslint": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-13.0.0.tgz", + "integrity": "sha512-P5K31qWgUOQCtJL/3tpvEe28KfP49qbr6MTVEXC7I2k7ci55bP3YDr+glhyCdhIzxGCVp2f8eobfQ5so52RIIA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" + "@typescript-eslint/parser": "^3.0.0", + "common-tags": "^1.4.0", + "dlv": "^1.1.0", + "eslint": "^7.9.0", + "indent-string": "^4.0.0", + "lodash.merge": "^4.6.0", + "loglevel-colored-level-prefix": "^1.0.0", + "prettier": "^2.0.0", + "pretty-format": "^23.0.1", + "require-relative": "^0.8.7", + "typescript": "^3.9.3", + "vue-eslint-parser": "~7.1.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10.0.0" } }, - "node_modules/parse-json/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/parse-json/node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "node_modules/prettier-eslint/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "devOptional": true, + "license": "MIT", "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "@babel/highlight": "^7.10.4" } }, - "node_modules/parse5-html-rewriting-stream": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", - "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "node_modules/prettier-eslint/node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", "dev": true, + "license": "MIT", "dependencies": { - "entities": "^4.3.0", - "parse5": "^7.0.0", - "parse5-sax-parser": "^7.0.0" + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/parse5-sax-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", - "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "node_modules/prettier-eslint/node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { - "parse5": "^7.0.0" + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">=10.10.0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/prettier-eslint/node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "license": "BSD-3-Clause" }, - "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "node_modules/prettier-eslint/node_modules/@typescript-eslint/parser": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", + "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.10.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-visitor-keys": "^1.1.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^10.12.0 || >=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", - "dev": true, - "engines": { - "node": ">=12" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/piscina": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.4.0.tgz", - "integrity": "sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==", - "dev": true, - "optionalDependencies": { - "nice-napi": "^1.0.2" + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "node_modules/prettier-eslint/node_modules/@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", "dev": true, - "dependencies": { - "find-up": "^6.3.0" - }, + "license": "MIT", "engines": { - "node": ">=14.16" + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "node_modules/prettier-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "^10.12.0 || >=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "node_modules/prettier-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", "dev": true, + "license": "MIT", "dependencies": { - "p-locate": "^6.0.0" + "eslint-visitor-keys": "^1.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "node_modules/prettier-eslint/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" + "license": "MIT", + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=0.4.0" + } + }, + "node_modules/prettier-eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "node_modules/prettier-eslint/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, + "license": "MIT", "dependencies": { - "p-limit": "^4.0.0" + "sprintf-js": "~1.0.2" + } + }, + "node_modules/prettier-eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/prettier-eslint/node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "^10.12.0 || >=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "node_modules/prettier-eslint/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8.0.0" } }, - "node_modules/pkg-dir/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "node_modules/prettier-eslint/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "node_modules/prettier-eslint/node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, + "license": "Apache-2.0", "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=10" } }, - "node_modules/postcss-loader": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", - "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "node_modules/prettier-eslint/node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "cosmiconfig": "^9.0.0", - "jiti": "^1.20.0", - "semver": "^7.5.4" + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" }, "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", - "dev": true - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "node_modules/prettier-eslint/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=4.0" } }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "node_modules/prettier-eslint/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" + "is-glob": "^4.0.1" }, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">= 6" } }, - "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "node_modules/prettier-eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.4" + "type-fest": "^0.20.2" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=8" }, - "peerDependencies": { - "postcss": "^8.1.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "node_modules/prettier-eslint/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, + "license": "MIT", "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">= 4" } }, - "node_modules/postcss-selector-parser": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", - "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "node_modules/prettier-eslint/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, + "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "engines": { - "node": ">=4" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "node_modules/prettier-eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/prettier-eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">= 0.8.0" + "node": "*" } }, - "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "node_modules/prettier-eslint/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, + "license": "MIT", "bin": { - "prettier": "bin/prettier.cjs" + "prettier": "bin-prettier.js" }, "engines": { - "node": ">=14" + "node": ">=10.13.0" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-eslint": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.3.0.tgz", - "integrity": "sha512-Lh102TIFCr11PJKUMQ2kwNmxGhTsv/KzUg9QYF2Gkw259g/kPgndZDWavk7/ycbRvj2oz4BPZ1gCU8bhfZH/Xg==", + "node_modules/prettier-eslint/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, - "dependencies": { - "@typescript-eslint/parser": "^6.7.5", - "common-tags": "^1.4.0", - "dlv": "^1.1.0", - "eslint": "^8.7.0", - "indent-string": "^4.0.0", - "lodash.merge": "^4.6.0", - "loglevel-colored-level-prefix": "^1.0.0", - "prettier": "^3.0.1", - "pretty-format": "^29.7.0", - "require-relative": "^0.8.7", - "typescript": "^5.2.2", - "vue-eslint-parser": "^9.1.0" - }, + "license": "BSD-3-Clause" + }, + "node_modules/prettier-eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=16.10.0" + "node": ">=10" }, - "peerDependencies": { - "prettier-plugin-svelte": "^3.0.0", - "svelte-eslint-parser": "*" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prettier-eslint/node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "peerDependenciesMeta": { - "prettier-plugin-svelte": { - "optional": true - }, - "svelte-eslint-parser": { - "optional": true - } + "engines": { + "node": ">=4.2.0" } }, "node_modules/prettier-linter-helpers": { @@ -13127,6 +13994,7 @@ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", "dev": true, + "license": "MIT", "dependencies": { "fast-diff": "^1.1.2" }, @@ -13135,57 +14003,78 @@ } }, "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" } }, "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "engines": { + "node": ">=4" + } + }, + "node_modules/pretty-format/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" } }, + "node_modules/pretty-format/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, "node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", "dev": true, + "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "license": "MIT" }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, + "license": "MIT", "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" @@ -13199,6 +14088,7 @@ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -13212,48 +14102,54 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "dev": true, + "license": "MIT", "optional": true }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "dev": true, - "engines": { - "node": ">=6" - } + "license": "MIT" }, "node_modules/qjobs": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.9" } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -13280,13 +14176,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -13296,6 +14194,7 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -13305,6 +14204,7 @@ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -13315,67 +14215,11 @@ "node": ">= 0.8" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, - "node_modules/read-package-json": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.1.tgz", - "integrity": "sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==", - "dev": true, - "dependencies": { - "glob": "^10.2.2", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/read-package-json-fast": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", - "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", - "dev": true, - "dependencies": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/read-package-json/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -13386,46 +14230,39 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, + "license": "MIT", "dependencies": { "regenerate": "^1.4.2" }, @@ -13437,33 +14274,50 @@ "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.4" } }, "node_modules/regex-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", - "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", - "dev": true + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } }, "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", "dev": true, + "license": "MIT", "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" }, @@ -13471,32 +14325,51 @@ "node": ">=4" } }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~0.5.0" + "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==", + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -13506,6 +14379,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -13514,19 +14388,22 @@ "version": "0.8.7", "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -13540,12 +14417,13 @@ } }, "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/resolve-url-loader": { @@ -13553,6 +14431,7 @@ "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", "dev": true, + "license": "MIT", "dependencies": { "adjust-sourcemap-loader": "^4.0.0", "convert-source-map": "^1.7.0", @@ -13569,6 +14448,7 @@ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, + "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -13583,21 +14463,26 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, + "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/retry": { @@ -13605,31 +14490,35 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", - "dev": true + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -13641,12 +14530,13 @@ } }, "node_modules/rollup": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", - "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", + "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -13656,32 +14546,39 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.2", - "@rollup/rollup-android-arm64": "4.17.2", - "@rollup/rollup-darwin-arm64": "4.17.2", - "@rollup/rollup-darwin-x64": "4.17.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", - "@rollup/rollup-linux-arm-musleabihf": "4.17.2", - "@rollup/rollup-linux-arm64-gnu": "4.17.2", - "@rollup/rollup-linux-arm64-musl": "4.17.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", - "@rollup/rollup-linux-riscv64-gnu": "4.17.2", - "@rollup/rollup-linux-s390x-gnu": "4.17.2", - "@rollup/rollup-linux-x64-gnu": "4.17.2", - "@rollup/rollup-linux-x64-musl": "4.17.2", - "@rollup/rollup-win32-arm64-msvc": "4.17.2", - "@rollup/rollup-win32-ia32-msvc": "4.17.2", - "@rollup/rollup-win32-x64-msvc": "4.17.2", + "@rollup/rollup-android-arm-eabi": "4.34.8", + "@rollup/rollup-android-arm64": "4.34.8", + "@rollup/rollup-darwin-arm64": "4.34.8", + "@rollup/rollup-darwin-x64": "4.34.8", + "@rollup/rollup-freebsd-arm64": "4.34.8", + "@rollup/rollup-freebsd-x64": "4.34.8", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", + "@rollup/rollup-linux-arm-musleabihf": "4.34.8", + "@rollup/rollup-linux-arm64-gnu": "4.34.8", + "@rollup/rollup-linux-arm64-musl": "4.34.8", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", + "@rollup/rollup-linux-riscv64-gnu": "4.34.8", + "@rollup/rollup-linux-s390x-gnu": "4.34.8", + "@rollup/rollup-linux-x64-gnu": "4.34.8", + "@rollup/rollup-linux-x64-musl": "4.34.8", + "@rollup/rollup-win32-arm64-msvc": "4.34.8", + "@rollup/rollup-win32-ia32-msvc": "4.34.8", + "@rollup/rollup-win32-x64-msvc": "4.34.8", "fsevents": "~2.3.2" } }, - "node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/run-parallel": { @@ -13703,14 +14600,16 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } @@ -13719,7 +14618,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -13733,27 +14631,43 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/safevalues": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", - "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==" + "dev": true, + "license": "MIT" }, "node_modules/sass": { - "version": "1.71.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", - "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", + "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", "dev": true, + "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", + "chokidar": "^4.0.0", + "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -13761,13 +14675,17 @@ }, "engines": { "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, "node_modules/sass-loader": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", - "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", + "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", "dev": true, + "license": "MIT", "dependencies": { "neo-async": "^2.6.2" }, @@ -13804,17 +14722,19 @@ } }, "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true, + "license": "ISC", "optional": true }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -13822,70 +14742,71 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/selfsigned": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" }, "engines": { "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, - "dependencies": { - "yallist": "^4.0.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -13910,6 +14831,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -13918,13 +14840,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/send/node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -13932,17 +14856,12 @@ "node": ">=4" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/send/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -13952,6 +14871,7 @@ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -13961,6 +14881,7 @@ "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.4", "batch": "0.6.1", @@ -13979,6 +14900,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -13988,6 +14910,7 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -13997,6 +14920,7 @@ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "dev": true, + "license": "MIT", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -14011,63 +14935,62 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/serve-index/node_modules/setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, + "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 0.8" } }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, + "license": "MIT", "dependencies": { "kind-of": "^6.0.2" }, @@ -14080,6 +15003,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -14092,29 +15016,92 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "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" }, "engines": { "node": ">= 0.4" @@ -14124,35 +15111,77 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/sigstore": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.0.tgz", - "integrity": "sha512-q+o8L2ebiWD1AxD17eglf1pFrl9jtW7FHa0ygqY6EKvibK8JHyq9Z26v9MZXeDiw+RbfOJ9j2v70M10Hd6E06A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz", + "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.3.1", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.1", - "@sigstore/sign": "^2.3.0", - "@sigstore/tuf": "^2.3.1", - "@sigstore/verify": "^1.2.0" + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "@sigstore/sign": "^3.1.0", + "@sigstore/tuf": "^3.1.0", + "@sigstore/verify": "^2.1.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/smart-buffer": { @@ -14160,22 +15189,24 @@ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, "node_modules/socket.io": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", - "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.2", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, @@ -14188,16 +15219,58 @@ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "~4.3.4", "ws": "~8.17.1" } }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dev": true, + "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -14206,11 +15279,48 @@ "node": ">=10.0.0" } }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", "dev": true, + "license": "MIT", "dependencies": { "faye-websocket": "^0.11.3", "uuid": "^8.3.2", @@ -14218,10 +15328,11 @@ } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "dev": true, + "license": "MIT", "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -14232,14 +15343,15 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", - "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "^7.1.1", + "agent-base": "^7.1.2", "debug": "^4.3.4", - "socks": "^2.7.1" + "socks": "^2.8.3" }, "engines": { "node": ">= 14" @@ -14250,15 +15362,17 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -14268,6 +15382,7 @@ "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", "dev": true, + "license": "MIT", "dependencies": { "iconv-lite": "^0.6.3", "source-map-js": "^1.0.2" @@ -14288,6 +15403,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -14300,6 +15416,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -14310,6 +15427,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -14319,6 +15437,7 @@ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -14328,29 +15447,33 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true + "dev": true, + "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, + "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", - "dev": true + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.0", "handle-thing": "^2.0.0", @@ -14367,6 +15490,7 @@ "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.0", "detect-node": "^2.0.4", @@ -14376,22 +15500,33 @@ "wbuf": "^1.7.3" } }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "license": "ISC", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/ssri": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", - "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/statuses": { @@ -14399,15 +15534,23 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, "node_modules/streamroller": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", "dev": true, + "license": "MIT", "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", @@ -14417,74 +15560,93 @@ "node": ">=8.0" } }, - "node_modules/streamroller/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/streamroller/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/streamroller/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "engines": { - "node": ">= 4.0.0" - } + "license": "MIT" }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-ansi": { @@ -14492,6 +15654,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -14505,6 +15668,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -14512,22 +15676,24 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/strip-json-comments": { @@ -14535,6 +15701,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -14542,33 +15709,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strong-log-transformer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", - "integrity": "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==", - "dev": true, - "dependencies": { - "duplexer": "^0.1.1", - "minimist": "^1.2.0", - "through": "^2.3.4" - }, - "bin": { - "sl-log-transformer": "bin/sl-log-transformer.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -14576,6 +15727,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -14588,24 +15740,93 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10" } }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", + "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", "dev": true, + "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.3", + "tslib": "^2.8.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/tapable": { @@ -14613,6 +15834,7 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -14622,6 +15844,7 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dev": true, + "license": "ISC", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -14634,27 +15857,12 @@ "node": ">=10" } }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -14667,6 +15875,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -14679,6 +15888,34 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { "node": ">=8" } @@ -14688,6 +15925,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, + "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" }, @@ -14699,13 +15937,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/terser": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", - "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -14720,16 +15960,17 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -14753,128 +15994,61 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } + "license": "MIT" }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, + "license": "Unlicense", "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" + "node": ">=10.18" }, - "engines": { - "node": "*" + "peerDependencies": { + "tslib": "^2" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "dev": true, + "license": "MIT", "dependencies": { - "rimraf": "^3.0.0" + "fdir": "^6.4.4", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=8.17.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, "engines": { - "node": ">=4" + "node": ">=0.6.0" } }, "node_modules/to-regex-range": { @@ -14882,6 +16056,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -14894,62 +16069,93 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6" } }, + "node_modules/tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, + "license": "MIT", "bin": { "tree-kill": "cli.js" } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, + "license": "MIT", "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "tslib": "^1.8.1" }, "engines": { - "node": ">=6" + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" }, "node_modules/tuf-js": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", - "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.0.1.tgz", + "integrity": "sha512-+68OP1ZzSF84rTckf3FA95vJ1Zlx/uaXyiiKyPd1pA4rZNkpEvDAKmsu1xUSmbF/chCRYgZ6UZkDwC7PmzmAyA==", "dev": true, + "license": "MIT", "dependencies": { - "@tufjs/models": "2.0.1", - "debug": "^4.3.4", - "make-fetch-happen": "^13.0.1" + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/type-check": { @@ -14957,6 +16163,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -14969,6 +16176,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -14981,6 +16189,7 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -14993,13 +16202,21 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15009,9 +16226,9 @@ } }, "node_modules/ua-parser-js": { - "version": "0.7.37", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", - "integrity": "sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==", + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", "dev": true, "funding": [ { @@ -15027,30 +16244,27 @@ "url": "https://github.com/sponsors/faisalman" } ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, "engines": { "node": "*" } }, - "node_modules/undici": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.11.1.tgz", - "integrity": "sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==", - "dev": true, - "engines": { - "node": ">=18.0" - } - }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -15060,6 +16274,7 @@ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, + "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" @@ -15069,10 +16284,11 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -15082,41 +16298,58 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", "dev": true, + "license": "ISC", "dependencies": { - "unique-slug": "^4.0.0" + "unique-slug": "^5.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", "dev": true, + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 10.0.0" + "node": ">= 4.0.0" } }, "node_modules/unpipe": { @@ -15124,14 +16357,15 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/update-browserslist-db": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", - "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -15147,9 +16381,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -15163,21 +16398,33 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4.0" } @@ -15187,27 +16434,37 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true, + "license": "MIT" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "node_modules/validate-npm-package-name": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", - "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", + "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", "dev": true, + "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/vary": { @@ -15215,25 +16472,31 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/vite": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.7.tgz", - "integrity": "sha512-sgnEEFTZYMui/sTlH1/XEnVNHMujOahPLGMxn1+5sIT45Xjng1Ec1K78jRP15dSmVgg5WBin9yO81j3o9OxofA==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz", + "integrity": "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -15242,18 +16505,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -15263,6 +16533,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -15271,413 +16544,377 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "node_modules/vite/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "node_modules/vite/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", "cpu": [ - "arm64" + "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", "cpu": [ - "ia32" + "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", "cpu": [ - "loong64" + "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "node_modules/vite/node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", "cpu": [ - "mips64el" + "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "node_modules/vite/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ - "sunos" + "linux" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], - "engines": { - "node": ">=12" - } + "peer": true }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], + "peer": true + }, + "node_modules/vite/node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, "engines": { - "node": ">=12" + "node": "^10 || ^12 || >=14" } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "node_modules/vite/node_modules/rollup": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", "dev": true, - "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.7" + }, "bin": { - "esbuild": "bin/esbuild" + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=12" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", + "fsevents": "~2.3.2" } }, "node_modules/void-elements": { @@ -15685,55 +16922,103 @@ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/vue-eslint-parser": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", - "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.1.1.tgz", + "integrity": "sha512-8FdXi0gieEwh1IprIBafpiJWcApwrU+l2FEj8c1HtHFdNXMd0+2jUSjBVmcQYohf/E72irwAXEXLga6TQcB3FA==", "dev": true, + "license": "MIT", "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" + "debug": "^4.1.1", + "eslint-scope": "^5.0.0", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.2.1", + "esquery": "^1.0.1", + "lodash": "^4.17.15" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": ">=8.10" }, "funding": { "url": "https://github.com/sponsors/mysticatea" }, "peerDependencies": { - "eslint": ">=6.0.0" + "eslint": ">=5.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" } }, "node_modules/vue-eslint-parser/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "estraverse": "^4.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, + "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -15747,6 +17032,7 @@ "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "dev": true, + "license": "MIT", "dependencies": { "minimalistic-assert": "^1.0.0" } @@ -15756,39 +17042,48 @@ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "dev": true, + "license": "MIT", "dependencies": { "defaults": "^1.0.3" } }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/webpack": { - "version": "5.90.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", - "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", - "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.21.10", + "version": "5.98.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", + "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -15808,19 +17103,21 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", - "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, + "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.12", + "memfs": "^4.6.0", "mime-types": "^2.1.31", + "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -15836,54 +17133,52 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", - "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", - "dev": true, - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", + "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", + "express": "^4.21.2", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", + "http-proxy-middleware": "^2.0.7", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.13.0" + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { @@ -15894,41 +17189,130 @@ } } }, - "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 8.10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "dev": true, + "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", - "wildcard": "^2.0.0" + "wildcard": "^2.0.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" } }, "node_modules/webpack-sources": { @@ -15936,6 +17320,7 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -15945,6 +17330,7 @@ "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", "dev": true, + "license": "MIT", "dependencies": { "typed-assert": "^1.0.8" }, @@ -15961,36 +17347,12 @@ } } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -16004,6 +17366,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -16012,37 +17375,15 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } + "license": "MIT" }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", @@ -16057,6 +17398,7 @@ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=0.8.0" } @@ -16066,6 +17408,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -16080,13 +17423,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -16096,6 +17441,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -16111,6 +17457,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -16123,89 +17470,87 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "license": "ISC" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=8.3.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "utf-8-validate": "^5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -16216,11 +17561,21 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -16229,13 +17584,15 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -16254,15 +17611,49 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -16270,13 +17661,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zone.js": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.5.tgz", - "integrity": "sha512-9XYWZzY6PhHOSdkYryNcMm7L8EK7a4q+GbTvxbIA2a9lMdRUpGuyaYvLDcg8D6bdn+JomSsbPcilVKg6SmUx6w==", - "dependencies": { - "tslib": "^2.3.0" + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zone.js": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", + "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==", + "license": "MIT" } } -} +} \ No newline at end of file diff --git a/modules/ui/package.json b/modules/ui/package.json index 7f83fc5f7..e209d38df 100644 --- a/modules/ui/package.json +++ b/modules/ui/package.json @@ -9,6 +9,7 @@ "test": "ng test", "test:coverage": "ng test --code-coverage", "test-headless": "ng test --browsers=ChromeHeadless --watch=false", + "test-ci": "ng test --no-watch --no-progress --code-coverage --browsers=ChromeHeadless", "docker": "docker rm -f test-run-ui && docker rmi test-run-ui && docker build -t test-run-ui . && docker run -d -p 80:80 --name test-run-ui test-run-ui", "lint": "ng lint", "lint:fix": "ng lint --fix", @@ -17,36 +18,39 @@ }, "private": true, "dependencies": { - "@angular/animations": "^17.0.8", - "@angular/cdk": "^17.0.4", - "@angular/common": "^17.0.8", - "@angular/compiler": "^17.0.8", - "@angular/core": "^17.0.8", - "@angular/forms": "^17.3.1", - "@angular/material": "^17.3.1", - "@angular/platform-browser": "^17.0.8", - "@angular/platform-browser-dynamic": "^17.3.1", - "@angular/router": "^17.3.1", - "@ngrx/component-store": "^17.1.1", - "@ngrx/effects": "^17.1.1", - "@ngrx/store": "^17.0.1", + "@angular/animations": "^19.0.1", + "@angular/cdk": "^19.0.1", + "@angular/common": "^19.0.1", + "@angular/compiler": "^19.0.1", + "@angular/core": "^19.0.1", + "@angular/forms": "^19.0.1", + "@angular/material": "^19.0.1", + "@angular/platform-browser": "^19.0.1", + "@angular/platform-browser-dynamic": "^19.0.1", + "@angular/router": "^19.0.1", + "@ngrx/component-store": "19.0.0-beta.0", + "@ngrx/effects": "19.0.0-beta.0", + "@ngrx/operators": "^19.0.0-beta.0", + "@ngrx/signals": "^19.0.0-beta.0", + "@ngrx/store": "19.0.0-beta.0", "ngx-mask": "^16.4.2", + "ngx-mqtt": "^17.0.0", "rxjs": "~7.8.0", "tslib": "^2.6.2", - "zone.js": "^0.14.4" + "zone.js": "^0.15.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.3.2", - "@angular-eslint/builder": "17.2.0", - "@angular-eslint/eslint-plugin": "17.2.0", - "@angular-eslint/eslint-plugin-template": "17.2.0", - "@angular-eslint/schematics": "17.2.0", - "@angular-eslint/template-parser": "17.2.0", - "@angular/cli": "~17.0.9", - "@angular/compiler-cli": "^17.3.1", + "@angular-devkit/build-angular": "^19.0.2", + "@angular-eslint/builder": "19.0.0-alpha.4", + "@angular-eslint/eslint-plugin": "19.0.0-alpha.4", + "@angular-eslint/eslint-plugin-template": "19.0.0-alpha.4", + "@angular-eslint/schematics": "19.0.0-alpha.4", + "@angular-eslint/template-parser": "19.0.0-alpha.4", + "@angular/cli": "~19.0.2", + "@angular/compiler-cli": "^19.0.1", "@types/jasmine": "~4.3.6", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", + "@typescript-eslint/eslint-plugin": "^8.2.0", + "@typescript-eslint/parser": "^8.2.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -57,7 +61,7 @@ "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "prettier": "^3.2.5", - "prettier-eslint": "^16.3.0", - "typescript": "~5.2.2" + "prettier-eslint": "^13.0.0", + "typescript": "~5.5.4" } } diff --git a/modules/ui/src/app/app-routing.module.ts b/modules/ui/src/app/app-routing.module.ts index a9864fe63..abf61afef 100644 --- a/modules/ui/src/app/app-routing.module.ts +++ b/modules/ui/src/app/app-routing.module.ts @@ -13,45 +13,65 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { Routes } from '@angular/router'; +import { ReportsComponent } from './pages/reports/reports.component'; +import { DevicesComponent } from './pages/devices/devices.component'; +import { CanDeactivateGuard } from './guards/can-deactivate.guard'; +import { TestrunComponent } from './pages/testrun/testrun.component'; +import { RiskAssessmentComponent } from './pages/risk-assessment/risk-assessment.component'; +import { CertificatesComponent } from './pages/certificates/certificates.component'; +import { SettingsComponent } from './pages/settings/settings.component'; +import { GeneralSettingsComponent } from './pages/general-settings/general-settings.component'; +import { CanActivateGuard } from './guards/can-activate.guard'; -const routes: Routes = [ +export const routes: Routes = [ + { + path: 'settings', + component: SettingsComponent, + title: 'Testrun - Settings', + children: [ + { + path: '', + redirectTo: 'general', + pathMatch: 'full', + }, + { + path: 'certificates', + component: CertificatesComponent, + title: 'Testrun - Certificates', + }, + { + path: 'general', + component: GeneralSettingsComponent, + title: 'Testrun - General Settings', + }, + ], + }, { path: 'testing', - loadChildren: () => - import('./pages/testrun/testrun.module').then(m => m.TestrunModule), - title: 'Testrun', + component: TestrunComponent, + title: 'Testrun - Testing', }, { path: 'devices', - loadChildren: () => - import('./pages/devices/devices.module').then(m => m.DevicesModule), + component: DevicesComponent, + canDeactivate: [CanDeactivateGuard], title: 'Testrun - Devices', }, { path: 'reports', - loadChildren: () => - import('./pages/reports/reports.module').then(m => m.ReportsModule), + component: ReportsComponent, title: 'Testrun - Reports', }, { path: 'risk-assessment', - loadChildren: () => - import('./pages/risk-assessment/risk-assessment.module').then( - m => m.RiskAssessmentModule - ), + component: RiskAssessmentComponent, + canDeactivate: [CanDeactivateGuard], title: 'Testrun - Risk Assessment', }, { path: '', - redirectTo: 'devices', - pathMatch: 'full', + canActivate: [CanActivateGuard], + component: DevicesComponent, }, ]; - -@NgModule({ - imports: [RouterModule.forRoot(routes, { useHash: true })], - exports: [RouterModule], -}) -export class AppRoutingModule {} diff --git a/modules/ui/src/app/app.component.html b/modules/ui/src/app/app.component.html index 38c210251..fe53f12c4 100644 --- a/modules/ui/src/app/app.component.html +++ b/modules/ui/src/app/app.component.html @@ -15,242 +15,144 @@ --> - - + +
- - - - - - - - + + + (consentShownEvent)="consentShown()">
- + aria-label="Testrun ui logo" + (keydown.enter)="onNavigationClick()">

Testrun

- - - - +
+ + + + + + +
- - - - No ports are detected. Please define a valid ones using - - - Selected port is missing! Please define a valid one using - - System settings - panel. - - - - Step 1: To perform a device test, please, select ports in - System settings - panel. - - - Step 2: To perform a device test please - Create a Device - first. - - - Step 3: Once device is created, you are able to - start testing. - - - Congratulations, the device is under test now! Do not forget to fill - Risk Assessment questionnaire. It is required to complete verification process. - +
- - - - - - - - + + +
Testrun mat-button routerLink="{{ route }}" routerLinkActive="app-sidebar-button-active" + (keydown.space)="onNavigationClick()" (keydown.enter)="onNavigationClick()"> - {{ icon }} + {{ + icon + }} {{ label }} + + +
+ + + No ports detected. Please connect and configure network and device + connections in the System settings panel. + + + Selected port is missing! Please define a valid one using System + settings panel. + + + + Further information is required in your device configurations. + Please update your Devices to continue testing. + + +
+
+ + +
+ + + + + + + + +
+
diff --git a/modules/ui/src/app/app.component.scss b/modules/ui/src/app/app.component.scss index 20e81c53e..3af857820 100644 --- a/modules/ui/src/app/app.component.scss +++ b/modules/ui/src/app/app.component.scss @@ -14,103 +14,77 @@ * limitations under the License. */ @use '@angular/material' as mat; -@import '../theming/colors'; -@import '../theming/variables'; +@use 'm3-theme' as *; +@use 'colors'; +@use 'variables'; -$toolbar-height: 56px; -$nav-close-width: 80px; -$nav-open-width: 236px; -$nav-close-btn-width: 48px; -$nav-open-btn-width: 210px; +$toolbar-height: 64px; +$content-padding-top: 18px; +$content-padding-bottom: 16px; +$nav-width: 96px; .app-container { height: 100%; + background-color: colors.$surface-container-low; } .spacer { flex: 1 1 auto; } -.mat-drawer-content { - height: calc(100% - 56px); - background: $white; -} - -.mat-drawer-side { - margin-top: $toolbar-height; -} - -.active-menu { - .app-sidebar { - width: $nav-open-width; - align-items: start; - } - - .app-sidebar-button { - width: $nav-open-btn-width; - height: $toolbar-height; - border-radius: 0 100px 100px 0; - margin: 0; - padding: 0 24px; - } - - .app-sidebar-button:first-child { - margin-top: 8px; - } - - .app-sidebar-button-active { - border: 1px solid mat.get-color-from-palette($color-primary, 50); - background-color: mat.get-color-from-palette($color-primary, 50); - } - - .app-sidebar-button-active > .mat-icon, - .sidebar-button-label { - color: $grey-800; - } - - .sidebar-button-label { - font-size: 14px; - font-weight: 500; - line-height: 20px; - letter-spacing: 0.25px; - } -} - .app-sidebar { display: flex; flex-direction: column; - background-color: $white; + background-color: colors.$surface-container-low; height: 100%; - gap: 8px; - width: $nav-close-width; + gap: 40px; + width: $nav-width; + align-items: center; + box-sizing: border-box; + padding-top: 104px; +} + +.nav-items-container { + width: 100%; + display: flex; + flex-direction: column; align-items: center; + justify-content: start; + gap: 4px; + flex-grow: 1; } -.app-sidebar-button, -.app-toolbar-button { - border-radius: 20px; +.app-sidebar-button { + display: flex; + flex-direction: column; + border-radius: variables.$corner-large; border: 1px solid transparent; min-width: 48px; - padding: 0; box-sizing: border-box; - height: 34px; - margin: 11px 0; + padding: 10px; line-height: 50% !important; + justify-content: center; + gap: 4px; + align-self: stretch; + height: unset; + width: 86px; + margin: 0 auto; } .app-sidebar-button { - width: $nav-close-btn-width; - display: flex; - justify-content: flex-start; - padding-inline: 8px; -} + --mat-text-button-with-icon-horizontal-padding: 8px; -.app-sidebar-button:first-child { - margin-top: 19px; + padding-inline: 8px; } -.app-toolbar-button-menu { - margin: 11px 17px 11px 16px; +.sidebar-button-label { + color: colors.$on-surface-variant; + text-align: center; + font-family: variables.$font-text; + font-size: 12px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.1px; } .app-sidebar-button:disabled { @@ -122,45 +96,80 @@ $nav-open-btn-width: 210px; margin-right: 0; width: 24px; font-size: 24px; - color: $dark-grey; + color: colors.$on-surface-variant; height: 24px; } .app-sidebar-button > .mat-icon { - margin: 0 3px; + margin: 4px; min-width: 24px; - line-height: 18px !important; } -.app-sidebar-button-active { - border: 1px solid mat.get-color-from-palette($color-primary, 500); - background-color: mat.get-color-from-palette($color-primary, 500); +.app-toolbar-button-help-tips { + display: none; +} + +.app-sidebar-button-active, +:host:has(app-help-tip) .app-toolbar-button-help-tips { + .material-symbols-outlined { + font-variation-settings: + 'FILL' 1, + 'wght' 400, + 'GRAD' 0, + 'opsz' 24; + } + + & > .mat-icon { + color: colors.$on-secondary-fixed-variant; + } + .sidebar-button-label { + color: colors.$secondary; + } + + .mat-mdc-button-persistent-ripple::before { + opacity: 1; + background: colors.$secondary-fixed; + } } -.app-sidebar-button-active > .mat-icon { - color: $white; +:host:has(app-help-tip) .app-toolbar-button-help-tips { + display: block; } .logo-link { - color: $grey-800; + color: colors.$on-surface; text-decoration: none; - font-size: 18px; + font-size: 22px; display: flex; flex-wrap: nowrap; align-items: center; - justify-content: center; - gap: 16px; - width: 120px; + gap: 10px; + flex-shrink: 0; + padding: 4px; + border: 1px solid transparent; + + &:focus-visible { + outline: none; + border: 1px solid colors.$black; + border-radius: 4px; + } + + &:active, + :focus-visible { + text-decoration: underline; + text-underline-offset: 2px; + } } .logo-link .mat-icon { - width: 36px; - height: 23px; - line-height: 18px !important; + width: 40px; + height: 26px; + flex-shrink: 0; + line-height: 22px !important; } .main-heading { - font-size: 18px; + font-size: 22px; line-height: 24px; } @@ -170,41 +179,67 @@ $nav-open-btn-width: 210px; left: 0; z-index: 3; height: $toolbar-height; - padding: 0; - background-color: $white; - border-bottom: 1px solid $light-grey; - color: $grey-800; + padding: 0 16px; + background-color: colors.$surface-container-low; + color: colors.$grey-800; + width: 100%; +} + +.app-bar-buttons { + display: flex; + padding: 4px 0 4px 24px; + justify-content: flex-end; + align-items: center; + gap: 8px; } .app-content { position: static; display: grid; grid-template-rows: 1fr; - margin-top: $toolbar-height; + margin-top: calc($toolbar-height + $content-padding-top); + height: calc( + 100% - $toolbar-height - $content-padding-top - $content-padding-bottom + ); + border-radius: variables.$corner-large; + background: colors.$white; } .app-content-main { position: relative; display: grid; - grid-template-rows: 0 auto; + grid-template-rows: auto 0 1fr; overflow: hidden; } .settings-drawer { width: 320px; box-shadow: none; - border-left: 1px solid $light-grey; -} - -.app-toolbar-button.app-toolbar-button-certificates { - margin-left: 72px; + border-left: 1px solid colors.$light-grey; } app-version { - margin-top: auto; margin-bottom: 16px; max-width: 100%; - width: $nav-close-width; + width: $nav-width; display: flex; justify-content: center; } + +:host { + display: block; + width: 100%; + height: 100%; + container-type: size; + container-name: app-root; +} +@container app-root (height < 600px) { + .app-sidebar { + gap: 4px; + padding-top: 82px; + } +} + +.closed-tip { + display: none; +} diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index 81e93b4b6..47ff1b0d8 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -31,7 +31,6 @@ import { MatIconModule } from '@angular/material/icon'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatSidenavModule } from '@angular/material/sidenav'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { AppRoutingModule } from './app-routing.module'; import SpyObj = jasmine.SpyObj; import { BypassComponent } from './components/bypass/bypass.component'; import { CalloutComponent } from './components/callout/callout.component'; @@ -42,31 +41,43 @@ import { import { Routes } from './model/routes'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { State } from '@ngrx/store'; -import { appFeatureKey } from './store/reducers'; import { FocusManagerService } from './services/focus-manager.service'; import { AppState } from './store/state'; +import { setIsOpenAddDevice } from './store/actions'; import { - setIsOpenAddDevice, - toggleMenu, - updateFocusNavigation, -} from './store/actions'; -import { - selectError, selectHasConnectionSettings, selectHasDevices, + selectHasExpiredDevices, selectHasRiskProfiles, selectInterfaces, + selectInternetConnection, + selectIsAllDevicesOutdated, selectIsOpenStartTestrun, selectIsOpenWaitSnackBar, - selectMenuOpened, + selectIsTestingComplete, + selectReports, + selectRiskProfiles, selectStatus, + selectSystemConfig, selectSystemStatus, } from './store/selectors'; import { MatIconTestingModule } from '@angular/material/icon/testing'; -import { CertificatesComponent } from './pages/certificates/certificates.component'; import { of } from 'rxjs'; import { WINDOW } from './providers/window.provider'; import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { HISTORY } from './mocks/reports.mock'; +import { TestRunMqttService } from './services/test-run-mqtt.service'; +import { MOCK_ADAPTERS } from './mocks/settings.mock'; +import { WifiComponent } from './components/wifi/wifi.component'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { Profile } from './model/profile'; +import { TestrunStatus } from './model/testrun-status'; +import { SpinnerComponent } from './components/spinner/spinner.component'; +import { ShutdownAppComponent } from './components/shutdown-app/shutdown-app.component'; +import { TestingCompleteComponent } from './components/testing-complete/testing-complete.component'; +import { VersionComponent } from './components/version/version.component'; +import { MOCK_MODULES } from './mocks/device.mock'; +import { HelpTips } from './model/tip-config'; const windowMock = { location: { @@ -81,9 +92,9 @@ describe('AppComponent', () => { let router: Router; let mockService: SpyObj; let store: MockStore; - let focusNavigation = true; let mockFocusManagerService: SpyObj; let mockLiveAnnouncer: SpyObj; + let mockMqttService: SpyObj; const enterKeyEvent = new KeyboardEvent('keydown', { key: 'Enter', @@ -108,20 +119,20 @@ describe('AppComponent', () => { 'getTestModules', 'testrunInProgress', 'fetchProfiles', - 'fetchCertificates', + 'getHistory', ]); - mockService.fetchCertificates.and.returnValue(of([])); mockFocusManagerService = jasmine.createSpyObj('mockFocusManagerService', [ 'focusFirstElementInContainer', ]); mockLiveAnnouncer = jasmine.createSpyObj('mockLiveAnnouncer', ['announce']); + mockMqttService = jasmine.createSpyObj(['getNetworkAdapters']); TestBed.configureTestingModule({ imports: [ + AppComponent, RouterTestingModule, HttpClientTestingModule, - AppRoutingModule, MatButtonModule, BrowserAnimationsModule, MatIconModule, @@ -130,55 +141,78 @@ describe('AppComponent', () => { BypassComponent, CalloutComponent, MatIconTestingModule, - CertificatesComponent, + WifiComponent, + MatTooltipModule, + FakeSpinnerComponent, + FakeShutdownAppComponent, + FakeVersionComponent, + FakeTestingCompleteComponent, + RouterTestingModule.withRoutes([ + { path: 'devices', children: [] }, + { path: 'settings', children: [] }, + { path: 'testing', children: [] }, + { path: 'reports', children: [] }, + ]), ], providers: [ { provide: TestRunService, useValue: mockService }, { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, + { provide: TestRunMqttService, useValue: mockMqttService }, { provide: State, - useValue: { - getValue: () => ({ - [appFeatureKey]: { - appComponent: { - focusNavigation: focusNavigation, - }, - }, - }), - }, + useValue: {}, }, provideMockStore({ selectors: [ { selector: selectInterfaces, value: {} }, { selector: selectHasConnectionSettings, value: true }, - { selector: selectError, value: null }, - { selector: selectMenuOpened, value: false }, + { selector: selectInternetConnection, value: true }, + { selector: selectSystemConfig, value: { network: {} } }, { selector: selectHasDevices, value: false }, + { selector: selectIsAllDevicesOutdated, value: false }, + { selector: selectHasExpiredDevices, value: false }, { selector: selectHasRiskProfiles, value: false }, { selector: selectStatus, value: null }, { selector: selectSystemStatus, value: null }, + { selector: selectIsTestingComplete, value: false }, + { selector: selectRiskProfiles, value: [] }, { selector: selectIsOpenStartTestrun, value: false }, { selector: selectIsOpenWaitSnackBar, value: false }, + { selector: selectReports, value: [] }, ], }), { provide: FocusManagerService, useValue: mockFocusManagerService }, { provide: WINDOW, useValue: windowMock }, ], - declarations: [ - AppComponent, - FakeGeneralSettingsComponent, - FakeSpinnerComponent, - FakeShutdownAppComponent, - FakeVersionComponent, - ], + }).overrideComponent(AppComponent, { + remove: { + imports: [ + SpinnerComponent, + ShutdownAppComponent, + TestingCompleteComponent, + VersionComponent, + ], + }, + add: { + imports: [ + FakeSpinnerComponent, + FakeShutdownAppComponent, + FakeVersionComponent, + FakeTestingCompleteComponent, + ], + }, }); + mockService.fetchDevices.and.returnValue(of([])); + mockService.getTestModules.and.returnValue(of([...MOCK_MODULES])); + mockMqttService.getNetworkAdapters.and.returnValue(of(MOCK_ADAPTERS)); store = TestBed.inject(MockStore); fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; router = TestBed.get(Router); compiled = fixture.nativeElement as HTMLElement; spyOn(store, 'dispatch').and.callFake(() => {}); + component.appStore.updateSettingMissedError(null); }); it('should create the app', () => { @@ -192,10 +226,10 @@ describe('AppComponent', () => { expect(sideBar).toBeDefined(); }); - it('should render menu button', () => { - const button = compiled.querySelector('.app-sidebar-button-menu'); + it('should render side button menu', () => { + const sideButtonMenu = compiled.querySelector('app-side-button-menu'); - expect(button).toBeDefined(); + expect(sideButtonMenu).toBeDefined(); }); it('should render runtime button', () => { @@ -277,138 +311,17 @@ describe('AppComponent', () => { expect(router.url).toBe(Routes.Reports); })); - it('should call toggleSettingsBtn focus when settingsDrawer close on closeSetting', fakeAsync(() => { - fixture.detectChanges(); - - spyOn(component.settingsDrawer, 'close').and.returnValue( - Promise.resolve('close') - ); - spyOn(component.toggleSettingsBtn, 'focus'); - - component.closeSetting(true); - tick(); - - component.settingsDrawer.close().then(() => { - expect(component.toggleSettingsBtn.focus).toHaveBeenCalled(); - }); - })); - - it('should call focusFirstElementInContainer if settingsDrawer opened not from toggleBtn', fakeAsync(() => { - fixture.detectChanges(); - - spyOn(component.settingsDrawer, 'close').and.returnValue( - Promise.resolve('close') - ); - - component.openGeneralSettings(false, false); - tick(); - component.closeSetting(false); - flush(); - - component.settingsDrawer.close().then(() => { - expect( - mockFocusManagerService.focusFirstElementInContainer - ).toHaveBeenCalled(); - }); - })); - - it('should update interfaces and config', () => { - fixture.detectChanges(); - - spyOn(component.settings, 'getSystemInterfaces'); - spyOn(component.settings, 'getSystemConfig'); - - component.openGeneralSettings(false, false); - - expect(component.settings.getSystemInterfaces).toHaveBeenCalled(); - expect(component.settings.getSystemConfig).toHaveBeenCalled(); - }); - - it('should call settingsDrawer open on openSetting', fakeAsync(() => { - fixture.detectChanges(); - spyOn(component.settingsDrawer, 'open'); - - component.openSetting(false); - tick(); - - expect(component.settingsDrawer.open).toHaveBeenCalledTimes(1); - })); - - it('should announce settingsDrawer disabled on openSetting and settings are disabled', fakeAsync(() => { - fixture.detectChanges(); - - spyOn(component.settingsDrawer, 'open').and.returnValue( - Promise.resolve('open') - ); - - component.openSetting(true); - tick(); - - expect(mockLiveAnnouncer.announce).toHaveBeenCalledWith( - 'The settings panel is disabled' - ); - })); - - it('should call settingsDrawer open on click settings button', () => { + it('should navigate to the settings when "settings" button is clicked', fakeAsync(() => { fixture.detectChanges(); - const settingsBtn = compiled.querySelector( + const settingsButton = compiled.querySelector( '.app-toolbar-button-general-settings' ) as HTMLButtonElement; - spyOn(component.settingsDrawer, 'open'); - - settingsBtn.click(); - - expect(component.settingsDrawer.open).toHaveBeenCalledTimes(1); - }); - - describe('menu button', () => { - beforeEach(() => { - mockFocusManagerService.focusFirstElementInContainer.calls.reset(); - store.overrideSelector(selectHasDevices, false); - fixture.detectChanges(); - }); - - it('should dispatch toggleMenu action', () => { - const menuBtn = compiled.querySelector( - '.app-toolbar-button-menu' - ) as HTMLButtonElement; - - menuBtn.click(); - - expect(store.dispatch).toHaveBeenCalledWith(toggleMenu()); - }); - - it('should focus navigation on tab press if menu button was clicked', () => { - focusNavigation = true; - const menuBtn = compiled.querySelector( - '.app-toolbar-button-menu' - ) as HTMLButtonElement; - - menuBtn.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' })); - const navigation = compiled.querySelector('.app-sidebar'); - - expect(store.dispatch).toHaveBeenCalledWith( - updateFocusNavigation({ focusNavigation: false }) - ); - expect( - mockFocusManagerService.focusFirstElementInContainer - ).toHaveBeenCalledWith(navigation); - }); - - it('should not focus navigation button on tab press if menu button was not clicked', () => { - focusNavigation = false; - const menuBtn = compiled.querySelector( - '.app-toolbar-button-menu' - ) as HTMLButtonElement; - - menuBtn.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' })); + settingsButton?.click(); + tick(); - expect( - mockFocusManagerService.focusFirstElementInContainer - ).not.toHaveBeenCalled(); - }); - }); + expect(router.url).toBe(Routes.Settings); + })); it('should have spinner', () => { const spinner = compiled.querySelector('app-spinner'); @@ -429,134 +342,91 @@ describe('AppComponent', () => { expect(version).toBeTruthy(); }); - describe('Callout component visibility', () => { - describe('with no connection settings', () => { - beforeEach(() => { - store.overrideSelector(selectHasConnectionSettings, false); - fixture.detectChanges(); - }); - - it('should have callout component with "Step 1" text', () => { - const callout = compiled.querySelector('app-callout'); - const calloutContent = callout?.innerHTML.trim(); - - expect(callout).toBeTruthy(); - expect(calloutContent).toContain('Step 1'); - }); - - it('should have callout content with "System settings" link ', () => { - const calloutLinkEl = compiled.querySelector( - '.message-link' - ) as HTMLAnchorElement; - const calloutLinkContent = calloutLinkEl.innerHTML.trim(); - - expect(calloutLinkEl).toBeTruthy(); - expect(calloutLinkContent).toContain('System settings'); - }); - - keyboardCases.forEach(testCase => { - it(`should call openSetting on keydown ${testCase.name} "Connection settings" link`, fakeAsync(() => { - const spyOpenSetting = spyOn(component, 'openSetting'); - const calloutLinkEl = compiled.querySelector( - '.message-link' - ) as HTMLAnchorElement; + it('should internet icon', () => { + fixture.detectChanges(); + const internet = compiled.querySelector('app-wifi'); - calloutLinkEl.dispatchEvent(testCase.event); - flush(); + expect(internet).toBeTruthy(); + }); - expect(spyOpenSetting).toHaveBeenCalled(); - })); - }); + describe('Testing complete', () => { + beforeEach(() => { + store.overrideSelector(selectIsTestingComplete, true); + fixture.detectChanges(); }); - describe('with system status as "Idle"', () => { - beforeEach(() => { - component.appStore.updateIsStatusLoaded(true); - store.overrideSelector(selectHasConnectionSettings, true); - store.overrideSelector(selectHasDevices, true); - store.overrideSelector(selectSystemStatus, MOCK_PROGRESS_DATA_IDLE); - - fixture.detectChanges(); - }); - - it('should have callout component with "Step 3" text', () => { - const callout = compiled.querySelector('app-callout'); - const calloutContent = callout?.innerHTML.trim(); + it('should have testing complete component', () => { + const testingCompleteComp = compiled.querySelector( + 'app-testing-complete' + ); - expect(callout).toBeTruthy(); - expect(calloutContent).toContain('Step 3'); - }); + expect(testingCompleteComp).toBeTruthy(); }); + }); - describe('with systemStatus data IN Progress and without riskProfiles', () => { + describe('Help tip component visibility', () => { + describe('with no connection settings', () => { beforeEach(() => { - store.overrideSelector(selectHasConnectionSettings, true); - store.overrideSelector(selectHasDevices, true); - store.overrideSelector(selectHasRiskProfiles, false); - store.overrideSelector( - selectStatus, - MOCK_PROGRESS_DATA_IN_PROGRESS.status - ); + store.overrideSelector(selectHasConnectionSettings, false); fixture.detectChanges(); }); - it('should have callout component with "Congratulations" text', () => { - const callout = compiled.querySelector('app-callout'); - const calloutContent = callout?.innerHTML.trim(); + it('should have help tip component with "Step 1" text', () => { + const helpTip = compiled.querySelector('app-help-tip'); + const helpTipTitle = compiled.querySelector('app-help-tip .title'); + const helpTipContent = helpTipTitle?.innerHTML.trim(); - expect(callout).toBeTruthy(); - expect(calloutContent).toContain('Congratulations'); + expect(helpTip).toBeTruthy(); + expect(helpTipContent).toContain('Step 1'); }); - it('should have callout component with "Risk Assessment" link', () => { - const callout = compiled.querySelector('app-callout'); - const calloutLinkEl = compiled.querySelector( - '.message-link' + it('should have help tip content with "Go to Settings" link ', () => { + const helpTipLinkEl = compiled.querySelector( + '.tip-action-link' ) as HTMLAnchorElement; - const calloutLinkContent = calloutLinkEl.innerHTML.trim(); + const helpTipLinkContent = helpTipLinkEl.innerHTML.trim(); - expect(callout).toBeTruthy(); - expect(calloutLinkContent).toContain('Risk Assessment'); + expect(helpTipLinkEl).toBeTruthy(); + expect(helpTipLinkContent).toContain('Go to Settings'); }); }); - describe('with no devices setted', () => { + describe('with no devices set', () => { beforeEach(() => { store.overrideSelector(selectHasDevices, false); fixture.detectChanges(); }); - it('should have callout component', () => { - const callout = compiled.querySelector('app-callout'); + it('should have helpTip component', () => { + const helpTip = compiled.querySelector('app-help-tip'); - expect(callout).toBeTruthy(); + expect(helpTip).toBeTruthy(); }); - it('should have callout component with "Step 2" text', () => { - const callout = compiled.querySelector('app-callout'); - const calloutContent = callout?.innerHTML.trim(); + it('should have help tip component with "Step 2" text', () => { + const helpTipTitle = compiled.querySelector('app-help-tip .title'); + const helpTipTitleContent = helpTipTitle?.innerHTML.trim(); - expect(callout).toBeTruthy(); - expect(calloutContent).toContain('Step 2'); + expect(helpTipTitleContent).toContain('Step 2'); }); - it('should have callout content with "Create a Device" link ', () => { - const calloutLinkEl = compiled.querySelector( - '.message-link' + it('should have help tip content with "Create Device" link ', () => { + const helpTipLinkEl = compiled.querySelector( + '.tip-action-link' ) as HTMLAnchorElement; - const calloutLinkContent = calloutLinkEl.innerHTML.trim(); + const helpTipLinkContent = helpTipLinkEl.innerHTML.trim(); - expect(calloutLinkEl).toBeTruthy(); - expect(calloutLinkContent).toContain('Create a Device'); + expect(helpTipLinkEl).toBeTruthy(); + expect(helpTipLinkContent).toContain('Device'); }); keyboardCases.forEach(testCase => { - it(`should navigate to the device-repository on keydown ${testCase.name} "Create a Device" link`, fakeAsync(() => { - const calloutLinkEl = compiled.querySelector( - '.message-link' + it(`should navigate to the device-repository on keydown ${testCase.name} "Create Device" link`, fakeAsync(() => { + const helpTipLinkEl = compiled.querySelector( + '.tip-action-link' ) as HTMLAnchorElement; - calloutLinkEl.dispatchEvent(testCase.event); + helpTipLinkEl.dispatchEvent(testCase.event); flush(); expect(router.url).toBe(Routes.Devices); @@ -564,11 +434,11 @@ describe('AppComponent', () => { }); it('should navigate to the device-repository on click "Create a Device" link', fakeAsync(() => { - const calloutLinkEl = compiled.querySelector( - '.message-link' + const helpTipLinkEl = compiled.querySelector( + '.tip-action-link' ) as HTMLAnchorElement; - calloutLinkEl.click(); + helpTipLinkEl.click(); flush(); expect(router.url).toBe(Routes.Devices); @@ -578,7 +448,35 @@ describe('AppComponent', () => { })); }); - describe('with devices setted but without systemStatus data', () => { + describe('with system status as "Idle"', () => { + beforeEach(() => { + component.appStore.updateIsStatusLoaded(true); + store.overrideSelector(selectHasConnectionSettings, true); + store.overrideSelector(selectHasDevices, true); + store.overrideSelector(selectSystemStatus, MOCK_PROGRESS_DATA_IDLE); + + fixture.detectChanges(); + }); + + it('should have help tip with "Step 3" title', () => { + const helpTipTitle = compiled.querySelector('app-help-tip .title'); + const helpTipTitleContent = helpTipTitle?.innerHTML.trim(); + + expect(helpTipTitleContent).toContain('Step 3'); + }); + + it('should NOT have help tip with "Step 3" if has reports', () => { + store.overrideSelector(selectReports, [...HISTORY]); + store.refreshState(); + fixture.detectChanges(); + + const helpTip = compiled.querySelector('app-help-tip'); + + expect(helpTip).toBeFalsy(); + }); + }); + + describe('with devices set but without systemStatus data', () => { beforeEach(() => { store.overrideSelector(selectHasDevices, true); component.appStore.updateIsStatusLoaded(true); @@ -588,73 +486,118 @@ describe('AppComponent', () => { fixture.detectChanges(); }); - it('should have callout component with "Step 3" text', () => { - const callout = compiled.querySelector('app-callout'); - const calloutContent = callout?.innerHTML.trim(); + it('should have help tip with "Step 3" text', () => { + const helpTipTitle = compiled.querySelector('app-help-tip .title'); + const helpTipTitleContent = helpTipTitle?.innerHTML.trim(); - expect(callout).toBeTruthy(); - expect(calloutContent).toContain('Step 3'); + expect(helpTipTitleContent).toContain('Step 3'); }); - it('should have callout component with "testing" link', () => { - const callout = compiled.querySelector('app-callout'); - const calloutLinkEl = compiled.querySelector( - '.message-link' + it('should have help tip with "Start Testrun" link', () => { + const helpTipLinkEl = compiled.querySelector( + '.tip-action-link' ) as HTMLAnchorElement; - const calloutLinkContent = calloutLinkEl.innerHTML.trim(); + const helpTipLinkContent = helpTipLinkEl.innerHTML.trim(); - expect(callout).toBeTruthy(); - expect(calloutLinkContent).toContain('testing'); + expect(helpTipLinkEl).toBeTruthy(); + expect(helpTipLinkContent).toContain(HelpTips.step3.action); }); keyboardCases.forEach(testCase => { - it(`should navigate to the runtime on keydown ${testCase.name} "Run the Test" link`, fakeAsync(() => { - const calloutLinkEl = compiled.querySelector( - '.message-link' + it(`should navigate to the testing on keydown ${testCase.name} "Start Testrun" link`, fakeAsync(() => { + const helpTipLinkEl = compiled.querySelector( + '.tip-action-link' ) as HTMLAnchorElement; - calloutLinkEl.dispatchEvent(testCase.event); + helpTipLinkEl.dispatchEvent(testCase.event); flush(); expect(router.url).toBe(Routes.Testing); })); }); + + it('should add "closed-tip" class to the tip on click "close" button on tip', fakeAsync(() => { + const helpTipEl = compiled.querySelector('app-help-tip') as HTMLElement; + const helpTipCloseBtn = compiled.querySelector( + 'app-help-tip .close-button' + ) as HTMLButtonElement; + + helpTipCloseBtn.click(); + tick(100); + + expect(helpTipEl.classList.contains('closed-tip')).toBeTrue(); + })); + + it('should remove "closed-tip" class from the tip on click toolbar "help tips" button', fakeAsync(() => { + const helpTipEl = compiled.querySelector('app-help-tip') as HTMLElement; + helpTipEl.classList.add('closed-tip'); + const helpTipsBtn = compiled.querySelector( + '.app-toolbar-button-help-tips' + ) as HTMLButtonElement; + + helpTipsBtn.click(); + tick(100); + + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalledWith(helpTipEl); + expect(helpTipEl.classList.contains('closed-tip')).toBeFalse(); + })); }); - describe('with devices setted, without systemStatus data, but run the tests ', () => { + describe('with devices set and systemStatus data', () => { beforeEach(() => { store.overrideSelector(selectHasDevices, true); + store.overrideSelector( + selectSystemStatus, + MOCK_PROGRESS_DATA_IN_PROGRESS + ); fixture.detectChanges(); }); - it('should not have callout component', () => { - const callout = compiled.querySelector('app-callout'); + it('should not have help tip', () => { + const helpTip = compiled.querySelector('app-help-tip'); - expect(callout).toBeNull(); + expect(helpTip).toBeNull(); }); }); - describe('with devices setted and systemStatus data', () => { + describe('with systemStatus data IN Progress and without riskProfiles', () => { beforeEach(() => { + store.overrideSelector(selectHasConnectionSettings, true); store.overrideSelector(selectHasDevices, true); + store.overrideSelector(selectHasRiskProfiles, false); store.overrideSelector( - selectSystemStatus, - MOCK_PROGRESS_DATA_IN_PROGRESS + selectStatus, + MOCK_PROGRESS_DATA_IN_PROGRESS.status ); fixture.detectChanges(); }); - it('should not have callout component', () => { - const callout = compiled.querySelector('app-callout'); + it('should have help tip with "Risk Assessment" title', () => { + const helpTipTitle = compiled.querySelector('app-help-tip .title'); + const helpTipTitleContent = helpTipTitle?.innerHTML.trim(); - expect(callout).toBeNull(); + expect(helpTipTitleContent).toContain('Risk Assessment'); + }); + + it('should have help tip with "Create risk profile" link', () => { + const helpTipLinkEl = compiled.querySelector( + '.tip-action-link' + ) as HTMLAnchorElement; + const helpTipLinkContent = helpTipLinkEl.innerHTML.trim(); + + expect(helpTipLinkEl).toBeTruthy(); + expect(helpTipLinkContent).toContain(HelpTips.step4.action); }); }); + }); + describe('Callout component visibility', () => { describe('error', () => { describe('with settingMissedError with one port is missed', () => { beforeEach(() => { - store.overrideSelector(selectError, { + component.appStore.updateSettingMissedError({ isSettingMissed: true, devicePortMissed: true, internetPortMissed: false, @@ -673,7 +616,7 @@ describe('AppComponent', () => { describe('with settingMissedError with two ports are missed', () => { beforeEach(() => { - store.overrideSelector(selectError, { + component.appStore.updateSettingMissedError({ isSettingMissed: true, devicePortMissed: true, internetPortMissed: true, @@ -686,13 +629,13 @@ describe('AppComponent', () => { const calloutContent = callout?.innerHTML.trim(); expect(callout).toBeTruthy(); - expect(calloutContent).toContain('No ports are detected.'); + expect(calloutContent).toContain('No ports detected.'); }); }); describe('with no settingMissedError', () => { beforeEach(() => { - store.overrideSelector(selectError, null); + component.appStore.updateSettingMissedError(null); store.overrideSelector(selectHasDevices, true); fixture.detectChanges(); }); @@ -703,54 +646,43 @@ describe('AppComponent', () => { }); }); }); - }); - - it('should not call toggleSettingsBtn focus on closeSetting when device length is 0', async () => { - fixture.detectChanges(); - spyOn(component.settingsDrawer, 'close').and.returnValue( - Promise.resolve('close') - ); - const spyToggle = spyOn(component.toggleSettingsBtn, 'focus'); - - await component.closeSetting(false); - - expect(spyToggle).toHaveBeenCalledTimes(0); - }); + describe('with expired devices', () => { + beforeEach(() => { + store.overrideSelector(selectHasExpiredDevices, true); + fixture.detectChanges(); + }); - it('should render certificates button', () => { - const generalSettingsButton = compiled.querySelector( - '.app-toolbar-button-certificates' - ); + it('should have callout component', () => { + const callouts = compiled.querySelectorAll('app-callout'); + let hasExpiredDeviceCallout = false; + callouts.forEach(callout => { + if ( + callout?.innerHTML + .trim() + .includes( + 'Further information is required in your device configurations.' + ) + ) { + hasExpiredDeviceCallout = true; + } + }); - expect(generalSettingsButton).toBeDefined(); + expect(hasExpiredDeviceCallout).toBeTrue(); + }); + }); }); - it('should call certificates open on click certificates button', () => { - fixture.detectChanges(); - - const settingsBtn = compiled.querySelector( - '.app-toolbar-button-certificates' - ) as HTMLButtonElement; - spyOn(component.certDrawer, 'open'); - - settingsBtn.click(); + it('should set focus to first focusable elem when close callout', fakeAsync(() => { + component.calloutClosed('mockId'); + tick(100); - expect(component.certDrawer.open).toHaveBeenCalledTimes(1); - }); + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalled(); + })); }); -@Component({ - selector: 'app-settings', - template: '
', -}) -class FakeGeneralSettingsComponent { - @Input() settingsDisable = false; - @Output() closeSettingEvent = new EventEmitter(); - getSystemInterfaces = () => {}; - getSystemConfig = () => {}; -} - @Component({ selector: 'app-spinner', template: '
', @@ -771,7 +703,14 @@ class FakeShutdownAppComponent { }) class FakeVersionComponent { @Input() consentShown!: boolean; - @Input() hasRiskProfiles!: boolean; @Output() consentShownEvent = new EventEmitter(); - @Output() navigateToRiskAssessmentEvent = new EventEmitter(); +} + +@Component({ + selector: 'app-testing-complete', + template: '
', +}) +class FakeTestingCompleteComponent { + @Input() profiles: Profile[] = []; + @Input() data!: TestrunStatus | null; } diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index 341f6bab5..2f1799022 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -13,89 +13,208 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, ElementRef, ViewChild } from '@angular/core'; -import { MatIconRegistry } from '@angular/material/icon'; +import { + AfterViewInit, + Component, + effect, + ElementRef, + viewChild, + inject, + ViewChild, + ChangeDetectorRef, + Renderer2, + HostListener, +} from '@angular/core'; +import { MatIconModule, MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; -import { MatDrawer } from '@angular/material/sidenav'; +import { MatSidenavModule } from '@angular/material/sidenav'; import { StatusOfTestrun } from './model/testrun-status'; -import { NavigationEnd, Router } from '@angular/router'; +import { NavigationEnd, Router, RouterModule } from '@angular/router'; import { CalloutType } from './model/callout-type'; import { Routes } from './model/routes'; import { FocusManagerService } from './services/focus-manager.service'; -import { State, Store } from '@ngrx/store'; +import { Store } from '@ngrx/store'; import { AppState } from './store/state'; -import { - setIsOpenAddDevice, - toggleMenu, - updateFocusNavigation, -} from './store/actions'; -import { appFeatureKey } from './store/reducers'; -import { SettingsComponent } from './pages/settings/settings.component'; +import { setIsOpenAddDevice, setIsOpenProfile } from './store/actions'; import { AppStore } from './app.store'; import { TestRunService } from './services/test-run.service'; -import { LiveAnnouncer } from '@angular/cdk/a11y'; import { filter, take } from 'rxjs/operators'; +import { timer } from 'rxjs'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatInputModule } from '@angular/material/input'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { TestingCompleteComponent } from './components/testing-complete/testing-complete.component'; +import { BypassComponent } from './components/bypass/bypass.component'; +import { ShutdownAppComponent } from './components/shutdown-app/shutdown-app.component'; +import { SpinnerComponent } from './components/spinner/spinner.component'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatButton, MatButtonModule } from '@angular/material/button'; +import { VersionComponent } from './components/version/version.component'; +import { MatSelectModule } from '@angular/material/select'; +import { WifiComponent } from './components/wifi/wifi.component'; +import { MatRadioModule } from '@angular/material/radio'; +import { CalloutComponent } from './components/callout/callout.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { SideButtonMenuComponent } from './components/side-button-menu/side-button-menu.component'; +import { Observable } from 'rxjs/internal/Observable'; +import { of } from 'rxjs/internal/observable/of'; +import { HelpTipComponent } from './components/help-tip/help-tip.component'; +import { HelpTips } from './model/tip-config'; + +export interface AddMenuItem { + icon?: string; + svgIcon?: string; + label: string; + description?: string; + onClick: () => void; + disabled$: Observable; +} -const DEVICES_LOGO_URL = '/assets/icons/devices.svg'; const DEVICES_RUN_URL = '/assets/icons/device_run.svg'; -const REPORTS_LOGO_URL = '/assets/icons/reports.svg'; -const RISK_ASSESSMENT_LOGO_URL = '/assets/icons/risk-assessment.svg'; const TESTRUN_LOGO_URL = '/assets/icons/testrun_logo_small.svg'; const TESTRUN_LOGO_COLOR_URL = '/assets/icons/testrun_logo_color.svg'; const CLOSE_URL = '/assets/icons/close.svg'; const DRAFT_URL = '/assets/icons/draft.svg'; +const PILOT_URL = '/assets/icons/pilot.svg'; +const QUALIFICATION_URL = '/assets/icons/qualification.svg'; + +const navKeys = [ + 'Tab', + 'ArrowUp', + 'ArrowDown', + 'ArrowLeft', + 'ArrowRight', + 'Home', + 'End', + 'PageUp', + 'PageDown', + 'Escape', + 'Enter', +]; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], + imports: [ + MatButtonModule, + MatIconModule, + MatToolbarModule, + MatSidenavModule, + MatButtonToggleModule, + MatRadioModule, + MatInputModule, + MatSelectModule, + MatTooltipModule, + ReactiveFormsModule, + MatFormFieldModule, + MatSnackBarModule, + SpinnerComponent, + BypassComponent, + VersionComponent, + CalloutComponent, + HelpTipComponent, + ShutdownAppComponent, + WifiComponent, + TestingCompleteComponent, + RouterModule, + CommonModule, + SideButtonMenuComponent, + ], providers: [AppStore], }) -export class AppComponent { +export class AppComponent implements AfterViewInit { + private matIconRegistry = inject(MatIconRegistry); + private domSanitizer = inject(DomSanitizer); + private route = inject(Router); + private store = inject>(Store); + private readonly focusManagerService = inject(FocusManagerService); + private testRunService = inject(TestRunService); + private cdr = inject(ChangeDetectorRef); + appStore = inject(AppStore); + private renderer = inject(Renderer2); + private el = inject(ElementRef); + public readonly CalloutType = CalloutType; public readonly StatusOfTestrun = StatusOfTestrun; + public readonly HelpTips = HelpTips; public readonly Routes = Routes; - private openedSettingFromToggleBtn = true; - - @ViewChild('settingsDrawer') public settingsDrawer!: MatDrawer; - @ViewChild('certDrawer') public certDrawer!: MatDrawer; - @ViewChild('toggleSettingsBtn') public toggleSettingsBtn!: HTMLButtonElement; - @ViewChild('toggleCertificatesBtn') - public toggleCertificatesBtn!: HTMLButtonElement; - @ViewChild('navigation') public navigation!: ElementRef; - @ViewChild('settings') public settings!: SettingsComponent; viewModel$ = this.appStore.viewModel$; - constructor( - private matIconRegistry: MatIconRegistry, - private domSanitizer: DomSanitizer, - private route: Router, - private store: Store, - private state: State, - private readonly focusManagerService: FocusManagerService, - private testRunService: TestRunService, - public appStore: AppStore, - private liveAnnouncer: LiveAnnouncer - ) { + readonly riskAssessmentLink = viewChild('riskAssessmentLink'); + private skipCount = 0; + @ViewChild('settingButton', { static: false }) settingButton!: MatButton; + settingTipTarget!: HTMLElement; + deviceTipTarget!: HTMLElement; + testingTipTarget!: HTMLElement; + riskAssessmentTipTarget!: HTMLElement; + isClosedTip = false; + + @HostListener('mousedown') + onMousedown() { + this.renderer.addClass(document.body as HTMLElement, 'using-mouse'); + } + + @HostListener('keydown', ['$event']) + onKeydown(e: KeyboardEvent) { + if (navKeys.includes(e.key)) { + this.renderer.removeClass(document.body as HTMLElement, 'using-mouse'); + } + } + + navigateToRuntime = () => { + this.route.navigate([Routes.Testing]); + this.appStore.setIsOpenStartTestrun(); + }; + + navigateToAddDevice = () => { + this.route.navigate([Routes.Devices]); + this.store.dispatch(setIsOpenAddDevice({ isOpenAddDevice: true })); + }; + + navigateToAddRiskAssessment = () => { + this.route.navigate([Routes.RiskAssessment]); + this.store.dispatch(setIsOpenProfile({ isOpenCreateProfile: true })); + }; + + menuItems: AddMenuItem[] = [ + { + svgIcon: 'testrun_logo_small', + label: 'Start Testing', + description: 'Configure your testing tasks', + onClick: this.navigateToRuntime, + disabled$: this.appStore.testrunButtonDisabled$, + }, + { + icon: 'home_iot_device', + label: 'Create new device', + onClick: this.navigateToAddDevice, + disabled$: of(false), + }, + { + icon: 'rule', + label: 'Create new Risk profile', + onClick: this.navigateToAddRiskAssessment, + disabled$: of(false), + }, + ]; + + constructor() { this.appStore.getDevices(); this.appStore.getRiskProfiles(); this.appStore.getSystemStatus(); - this.matIconRegistry.addSvgIcon( - 'devices', - this.domSanitizer.bypassSecurityTrustResourceUrl(DEVICES_LOGO_URL) - ); + this.appStore.getReports(); + this.appStore.getTestModules(); + this.appStore.getNetworkAdapters(); + this.appStore.getInterfaces(); + this.appStore.getSystemConfig(); this.matIconRegistry.addSvgIcon( 'device_run', this.domSanitizer.bypassSecurityTrustResourceUrl(DEVICES_RUN_URL) ); - this.matIconRegistry.addSvgIcon( - 'reports', - this.domSanitizer.bypassSecurityTrustResourceUrl(REPORTS_LOGO_URL) - ); - this.matIconRegistry.addSvgIcon( - 'risk_assessment', - this.domSanitizer.bypassSecurityTrustResourceUrl(RISK_ASSESSMENT_LOGO_URL) - ); this.matIconRegistry.addSvgIcon( 'testrun_logo_small', this.domSanitizer.bypassSecurityTrustResourceUrl(TESTRUN_LOGO_URL) @@ -112,80 +231,86 @@ export class AppComponent { 'draft', this.domSanitizer.bypassSecurityTrustResourceUrl(DRAFT_URL) ); + this.matIconRegistry.addSvgIcon( + 'pilot', + this.domSanitizer.bypassSecurityTrustResourceUrl(PILOT_URL) + ); + this.matIconRegistry.addSvgIcon( + 'qualification', + this.domSanitizer.bypassSecurityTrustResourceUrl(QUALIFICATION_URL) + ); + effect(() => { + if (this.skipCount === 0 && this.riskAssessmentLink()) { + this.riskAssessmentLink()?.nativeElement.focus(); + } else if (this.skipCount > 0) { + this.skipCount--; + } + }); } - get isRiskAssessmentRoute(): boolean { - return this.route.url === Routes.RiskAssessment; - } - - navigateToDeviceRepository(): void { - this.route.navigate([Routes.Devices]); - this.store.dispatch(setIsOpenAddDevice({ isOpenAddDevice: true })); - } - - navigateToRuntime(): void { - this.route.navigate([Routes.Testing]); - this.appStore.setIsOpenStartTestrun(); - } + ngAfterViewInit() { + this.settingTipTarget = this.settingButton._elementRef.nativeElement; + this.deviceTipTarget = document.querySelector( + '.app-sidebar-button.app-sidebar-button-devices' + ) as HTMLElement; + this.testingTipTarget = document.querySelector( + '.app-sidebar-button.app-sidebar-button-testrun' + ) as HTMLElement; - navigateToRiskAssessment(): void { - this.route.navigate([Routes.RiskAssessment]); - } + this.riskAssessmentTipTarget = document.querySelector( + '.app-sidebar-button.app-sidebar-button-risk-assessment' + ) as HTMLElement; - async closeCertificates(): Promise { - await this.certDrawer.close(); - } + this.viewModel$ + .pipe( + filter(({ isStatusLoaded }) => isStatusLoaded === true), + take(1) + ) + .subscribe(({ systemStatus }) => { + if (systemStatus === StatusOfTestrun.InProgress) { + // link should not be focused after page is just loaded + this.skipCount = 1; + } + }); - async closeSetting(hasDevices: boolean): Promise { - return await this.settingsDrawer.close().then(() => { - if (hasDevices) { - this.toggleSettingsBtn.focus(); - } // else device create window will be opened - if (!this.openedSettingFromToggleBtn) { - this.focusManagerService.focusFirstElementInContainer(); - } - }); + this.cdr.detectChanges(); } - async openSetting(isSettingsDisabled: boolean): Promise { - return await this.openGeneralSettings(false, isSettingsDisabled); + get isDevicesRoute(): boolean { + return this.route.url === Routes.Devices; } - public toggleMenu(event: MouseEvent) { - event.stopPropagation(); - this.store.dispatch(toggleMenu()); + navigateToDeviceRepository(): void { + this.route.navigate([Routes.Devices]); } - /** - * When side menu is opened - */ - skipToNavigation(event: Event) { - if (this.state.getValue()[appFeatureKey].appComponent.focusNavigation) { - event.preventDefault(); // if not prevented, second element will be focused - this.focusManagerService.focusFirstElementInContainer( - this.navigation.nativeElement - ); - this.store.dispatch(updateFocusNavigation({ focusNavigation: false })); // user will be navigated according to normal flow on tab - } + navigateToSettings(): void { + this.route.navigate([Routes.Settings]).then(() => { + timer(100).subscribe(() => { + this.appStore.setFocusOnPage( + window.document.querySelector('app-general-settings') + ); + }); + }); } - async openGeneralSettings( - openSettingFromToggleBtn: boolean, - isSettingsDisabled: boolean - ) { - this.openedSettingFromToggleBtn = openSettingFromToggleBtn; - this.settings.getSystemInterfaces(); - this.settings.getSystemConfig(); - await this.settingsDrawer.open(); - if (isSettingsDisabled) { - await this.liveAnnouncer.announce('The settings panel is disabled'); - } - } + onCLoseTip(isClosed: boolean): void { + this.isClosedTip = isClosed; + const helpTipButton = window.document.querySelector( + '.app-toolbar-button-help-tips' + ) as HTMLButtonElement; + const helpTipEl = this.el.nativeElement.querySelector('app-help-tip'); - async openCert() { - await this.certDrawer.open(); + timer(100).subscribe(() => { + if (isClosed) { + this.renderer.addClass(helpTipEl, 'closed-tip'); + helpTipButton.focus(); + } else { + this.renderer.removeClass(helpTipEl, 'closed-tip'); + this.focusManagerService.focusFirstElementInContainer(helpTipEl); + } + }); } - consentShown() { this.appStore.setContent(); } @@ -201,7 +326,17 @@ export class AppComponent { take(1) ) .subscribe(() => { - this.appStore.setFocusOnPage(); + const mainContainer = window.document.querySelector('#main'); + this.appStore.setFocusOnPage(mainContainer); + }); + } + + calloutClosed(id: string | null) { + if (id) { + this.appStore.setCloseCallout(id); + timer(100).subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(); }); + } } } diff --git a/modules/ui/src/app/app.module.ts b/modules/ui/src/app/app.module.ts deleted file mode 100644 index 78621a464..000000000 --- a/modules/ui/src/app/app.module.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; -import { NgModule } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatButtonToggleModule } from '@angular/material/button-toggle'; -import { MatIconModule } from '@angular/material/icon'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatRadioModule } from '@angular/material/radio'; -import { BrowserModule } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -import { AppRoutingModule } from './app-routing.module'; -import { AppComponent } from './app.component'; -import { SettingsComponent } from './pages/settings/settings.component'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { MatInputModule } from '@angular/material/input'; -import { MatSelectModule } from '@angular/material/select'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { ErrorInterceptor } from './interceptors/error.interceptor'; -import { LoadingInterceptor } from './interceptors/loading.interceptor'; -import { SpinnerComponent } from './components/spinner/spinner.component'; -import { BypassComponent } from './components/bypass/bypass.component'; -import { VersionComponent } from './components/version/version.component'; -import { CalloutComponent } from './components/callout/callout.component'; -import { StoreModule } from '@ngrx/store'; -import { appFeatureKey, rootReducer } from './store/reducers'; -import { EffectsModule } from '@ngrx/effects'; -import { AppEffects } from './store/effects'; -import { CdkTrapFocus } from '@angular/cdk/a11y'; -import { SettingsDropdownComponent } from './pages/settings/components/settings-dropdown/settings-dropdown.component'; -import { ShutdownAppComponent } from './components/shutdown-app/shutdown-app.component'; -import { WindowProvider } from './providers/window.provider'; -import { CertificatesComponent } from './pages/certificates/certificates.component'; -import { LOADER_TIMEOUT_CONFIG_TOKEN } from './services/loaderConfig'; - -@NgModule({ - declarations: [AppComponent, SettingsComponent], - imports: [ - BrowserModule, - AppRoutingModule, - NoopAnimationsModule, - MatButtonModule, - MatIconModule, - MatToolbarModule, - MatSidenavModule, - MatButtonToggleModule, - MatRadioModule, - MatInputModule, - MatSelectModule, - MatTooltipModule, - HttpClientModule, - ReactiveFormsModule, - MatFormFieldModule, - MatSnackBarModule, - SpinnerComponent, - BypassComponent, - VersionComponent, - CalloutComponent, - StoreModule.forRoot({ [appFeatureKey]: rootReducer }), - EffectsModule.forRoot([AppEffects]), - CdkTrapFocus, - SettingsDropdownComponent, - ShutdownAppComponent, - CertificatesComponent, - ], - providers: [ - WindowProvider, - { - provide: HTTP_INTERCEPTORS, - useClass: ErrorInterceptor, - multi: true, - }, - { - provide: HTTP_INTERCEPTORS, - useClass: LoadingInterceptor, - multi: true, - }, - { provide: LOADER_TIMEOUT_CONFIG_TOKEN, useValue: 1000 }, - ], - bootstrap: [AppComponent], -}) -export class AppModule {} diff --git a/modules/ui/src/app/app.store.spec.ts b/modules/ui/src/app/app.store.spec.ts index 2bdf63195..94c0db89d 100644 --- a/modules/ui/src/app/app.store.spec.ts +++ b/modules/ui/src/app/app.store.spec.ts @@ -15,31 +15,47 @@ */ import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { of, skip, take } from 'rxjs'; -import { AppStore, CONSENT_SHOWN_KEY } from './app.store'; +import { AppStore, CALLOUT_STATE_KEY, CONSENT_SHOWN_KEY } from './app.store'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { AppState } from './store/state'; import { - selectError, selectHasConnectionSettings, selectHasDevices, + selectHasExpiredDevices, selectHasRiskProfiles, selectInterfaces, + selectInternetConnection, + selectIsAllDevicesOutdated, selectIsOpenWaitSnackBar, - selectMenuOpened, + selectIsTestingComplete, + selectReports, + selectRiskProfiles, selectStatus, + selectSystemConfig, + selectSystemStatus, + selectTestModules, } from './store/selectors'; import { TestRunService } from './services/test-run.service'; import SpyObj = jasmine.SpyObj; -import { device } from './mocks/device.mock'; +import { device, MOCK_MODULES, MOCK_TEST_MODULES } from './mocks/device.mock'; import { + fetchReports, fetchRiskProfiles, fetchSystemStatus, setDevices, + updateAdapters, + setTestModules, + fetchSystemConfig, + fetchInterfaces, } from './store/actions'; import { MOCK_PROGRESS_DATA_IN_PROGRESS } from './mocks/testrun.mock'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NotificationService } from './services/notification.service'; import { FocusManagerService } from './services/focus-manager.service'; +import { TestRunMqttService } from './services/test-run-mqtt.service'; +import { MOCK_ADAPTERS } from './mocks/settings.mock'; +import { TestingType } from './model/device'; +import { ResultOfTestrun, StatusOfTestrun } from './model/testrun-status'; const mock = (() => { let store: { [key: string]: string } = {}; @@ -50,6 +66,12 @@ const mock = (() => { setItem: (key: string, value: string) => { store[key] = value + ''; }, + getObject: (key: string) => { + return store[key] || null; + }, + setObject: (key: string, value: object) => { + store[key] = JSON.stringify(value); + }, clear: () => { store = {}; }, @@ -65,15 +87,22 @@ describe('AppStore', () => { let mockService: SpyObj; let mockNotificationService: SpyObj; let mockFocusManagerService: SpyObj; + let mockMqttService: SpyObj; beforeEach(() => { - mockService = jasmine.createSpyObj('mockService', ['fetchDevices']); + window.sessionStorage.clear(); + + mockService = jasmine.createSpyObj('mockService', [ + 'fetchDevices', + 'getTestModules', + ]); mockNotificationService = jasmine.createSpyObj('mockNotificationService', [ 'notify', ]); mockFocusManagerService = jasmine.createSpyObj([ 'focusFirstElementInContainer', ]); + mockMqttService = jasmine.createSpyObj(['getNetworkAdapters']); TestBed.configureTestingModule({ providers: [ @@ -82,11 +111,20 @@ describe('AppStore', () => { selectors: [ { selector: selectStatus, value: null }, { selector: selectIsOpenWaitSnackBar, value: false }, + { selector: selectTestModules, value: MOCK_TEST_MODULES }, + { selector: selectInternetConnection, value: false }, + { selector: selectIsAllDevicesOutdated, value: false }, + { selector: selectSystemStatus, value: null }, + { selector: selectIsTestingComplete, value: false }, + { selector: selectRiskProfiles, value: [] }, + { selector: selectSystemConfig, value: { network: {} } }, + { selector: selectInterfaces, value: {} }, ], }), { provide: TestRunService, useValue: mockService }, { provide: NotificationService, useValue: mockNotificationService }, { provide: FocusManagerService, useValue: mockFocusManagerService }, + { provide: TestRunMqttService, useValue: mockMqttService }, ], imports: [BrowserAnimationsModule], }); @@ -95,11 +133,11 @@ describe('AppStore', () => { appStore = TestBed.inject(AppStore); store.overrideSelector(selectHasDevices, true); + store.overrideSelector(selectHasExpiredDevices, true); store.overrideSelector(selectHasRiskProfiles, false); + store.overrideSelector(selectReports, []); store.overrideSelector(selectHasConnectionSettings, true); - store.overrideSelector(selectMenuOpened, true); store.overrideSelector(selectInterfaces, {}); - store.overrideSelector(selectError, null); store.overrideSelector(selectStatus, null); spyOn(store, 'dispatch').and.callFake(() => {}); @@ -139,13 +177,20 @@ describe('AppStore', () => { expect(store).toEqual({ consentShown: false, hasDevices: true, + hasExpiredDevices: true, + isAllDevicesOutdated: false, hasRiskProfiles: false, + reports: [], isStatusLoaded: false, systemStatus: null, + testrunStatus: null, + isTestingComplete: false, + riskProfiles: [], hasConnectionSettings: true, - isMenuOpen: true, interfaces: {}, settingMissedError: null, + calloutState: new Map(), + hasInternetConnection: false, }); done(); }); @@ -217,7 +262,7 @@ describe('AppStore', () => { describe('setFocusOnPage', () => { it('should call focusFirstElementInContainer', fakeAsync(() => { - appStore.setFocusOnPage(); + appStore.setFocusOnPage(null); tick(101); @@ -226,5 +271,273 @@ describe('AppStore', () => { ).toHaveBeenCalled(); })); }); + + describe('getReports', () => { + it('should dispatch fetchReports', () => { + appStore.getReports(); + + expect(store.dispatch).toHaveBeenCalledWith(fetchReports()); + }); + }); + + describe('getTestModules', () => { + const modules = [...MOCK_MODULES]; + + beforeEach(() => { + mockService.getTestModules.and.returnValue(of(modules)); + }); + + it('should dispatch action setDevices', () => { + appStore.getTestModules(); + + expect(store.dispatch).toHaveBeenCalledWith( + setTestModules({ + testModules: [ + { + displayName: 'Connection', + name: 'connection', + enabled: true, + }, + { + displayName: 'Udmi', + name: 'udmi', + enabled: true, + }, + ], + }) + ); + }); + }); + + describe('getNetworkAdapters', () => { + const adapters = MOCK_ADAPTERS; + + beforeEach(() => { + mockMqttService.getNetworkAdapters.and.returnValue(of(adapters)); + }); + + it('should dispatch action setDevices', () => { + appStore.getNetworkAdapters(); + + expect(store.dispatch).toHaveBeenCalledWith( + updateAdapters({ adapters }) + ); + }); + + it('should notify about new adapters', () => { + appStore.getNetworkAdapters(); + + expect(mockNotificationService.notify).toHaveBeenCalledWith( + 'New network adapter(s) mockNewInternetKey has been detected. You can switch to using it in the System settings menu' + ); + }); + }); + + describe('setCloseCallout', () => { + it('should update store', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.calloutState.get('test')).toEqual(true); + done(); + }); + + appStore.setCloseCallout('test'); + }); + + it('should update storage', () => { + appStore.setCloseCallout('test'); + + expect(mock.getObject(CALLOUT_STATE_KEY)).toBeTruthy(); + }); + }); + + describe('checkInterfacesInConfig', () => { + it('should update settingMissedError with all false if all ports are present', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: false, + devicePortMissed: false, + internetPortMissed: false, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, { + enx00e04c020fa8: '00:e0:4c:02:0f:a8', + enx207bd26205e9: '20:7b:d2:62:05:e9', + }); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: 'enx00e04c020fa8', + internet_intf: 'enx207bd26205e9', + }, + }); + store.refreshState(); + }); + + it('should update settingMissedError with all true if all ports are missing', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: true, + devicePortMissed: true, + internetPortMissed: true, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, { + enx00e04c020fa9: '00:e0:4c:02:0f:a8', + enx207bd26205e8: '20:7b:d2:62:05:e9', + }); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: 'enx00e04c020fa8', + internet_intf: 'enx207bd26205e9', + }, + }); + store.refreshState(); + }); + + it('should update settingMissedError with devicePortMissed true if device port is missing', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: true, + devicePortMissed: true, + internetPortMissed: false, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, { + enx00e04c020fa9: '00:e0:4c:02:0f:a8', + enx207bd26205e8: '20:7b:d2:62:05:e9', + }); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: 'enx00e04c020fa8', + internet_intf: 'enx207bd26205e8', + }, + }); + store.refreshState(); + }); + + it('should update settingMissedError with internetPortMissed true if device internet is missing', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: true, + devicePortMissed: false, + internetPortMissed: true, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, { + enx00e04c020fa9: '00:e0:4c:02:0f:a8', + enx207bd26205e8: '20:7b:d2:62:05:e9', + }); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: 'enx00e04c020fa9', + internet_intf: 'enx207bd26205e9', + }, + }); + store.refreshState(); + }); + + it('should update settingMissedError with all false if interface are not empty and config is not set', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: false, + devicePortMissed: false, + internetPortMissed: false, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, { + enx00e04c020fa8: '00:e0:4c:02:0f:a8', + enx207bd26205e9: '20:7b:d2:62:05:e9', + }); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: '', + internet_intf: '', + }, + }); + store.refreshState(); + }); + + it('should update settingMissedError with all false if interface are empty and config is not set', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: false, + devicePortMissed: false, + internetPortMissed: false, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, {}); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: '', + internet_intf: '', + }, + }); + store.refreshState(); + }); + + it('should send GA event', done => { + // @ts-expect-error data layer should be defined + window.dataLayer = window.dataLayer || []; + + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(() => { + expect( + // @ts-expect-error data layer should be defined + window.dataLayer.some(event => event.event === 'pilot_is_compliant') + ).toBeTrue(); + done(); + }); + + store.overrideSelector(selectIsTestingComplete, true); + store.overrideSelector(selectSystemStatus, { + result: ResultOfTestrun.Compliant, + status: StatusOfTestrun.Complete, + mac_addr: '00:1e:42:35:73:c4', + device: { + manufacturer: 'Delta', + model: '03-DIN-CPU', + mac_addr: '00:1e:42:35:73:c4', + firmware: '1.2.2', + test_pack: TestingType.Pilot, + }, + started: '2023-06-22T09:20:00.123Z', + finished: '2023-06-22T09:26:00.123Z', + report: 'https://api.testrun.io/report.pdf', + export: 'https://api.testrun.io/export.pdf', + tags: [], + tests: { + total: 3, + results: [], + }, + }); + store.refreshState(); + }); + }); + + describe('getInterfaces', () => { + it('should dispatch action fetchInterfaces', () => { + appStore.getInterfaces(); + + expect(store.dispatch).toHaveBeenCalledWith(fetchInterfaces()); + }); + }); + + describe('getSystemConfig', () => { + it('should dispatch action fetchSystemConfig', () => { + appStore.getSystemConfig(); + + expect(store.dispatch).toHaveBeenCalledWith(fetchSystemConfig()); + }); + }); }); }); diff --git a/modules/ui/src/app/app.store.ts b/modules/ui/src/app/app.store.ts index 9bd8dcff4..7f89b544f 100644 --- a/modules/ui/src/app/app.store.ts +++ b/modules/ui/src/app/app.store.ts @@ -14,65 +14,147 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { ComponentStore } from '@ngrx/component-store'; import { tap } from 'rxjs/operators'; import { - selectError, selectHasConnectionSettings, selectHasDevices, + selectHasExpiredDevices, selectHasRiskProfiles, selectInterfaces, - selectMenuOpened, + selectInternetConnection, + selectIsAllDevicesOutdated, + selectIsTestingComplete, + selectReports, + selectRiskProfiles, selectStatus, + selectSystemConfig, + selectSystemStatus, } from './store/selectors'; import { Store } from '@ngrx/store'; import { AppState } from './store/state'; import { TestRunService } from './services/test-run.service'; -import { delay, exhaustMap, Observable, skip } from 'rxjs'; -import { Device } from './model/device'; +import { + combineLatest, + delay, + exhaustMap, + filter, + Observable, + skip, +} from 'rxjs'; +import { Device, TestingType, TestModule } from './model/device'; import { setDevices, setIsOpenStartTestrun, fetchSystemStatus, fetchRiskProfiles, + fetchReports, + setTestModules, + updateAdapters, + fetchInterfaces, + fetchSystemConfig, } from './store/actions'; -import { TestrunStatus } from './model/testrun-status'; -import { SettingMissedError, SystemInterfaces } from './model/setting'; +import { ResultOfTestrun, TestrunStatus } from './model/testrun-status'; +import { + Adapters, + SettingMissedError, + SystemConfig, + SystemInterfaces, +} from './model/setting'; import { FocusManagerService } from './services/focus-manager.service'; +import { TestRunMqttService } from './services/test-run-mqtt.service'; +import { NotificationService } from './services/notification.service'; +import { Profile } from './model/profile'; +import { map } from 'rxjs/internal/operators/map'; export const CONSENT_SHOWN_KEY = 'CONSENT_SHOWN'; +export const CALLOUT_STATE_KEY = 'CALLOUT_STATE'; export interface AppComponentState { consentShown: boolean; isStatusLoaded: boolean; systemStatus: TestrunStatus | null; + calloutState: Map; + settingMissedError: SettingMissedError | null; } @Injectable() export class AppStore extends ComponentStore { + private store = inject>(Store); + private testRunService = inject(TestRunService); + private testRunMqttService = inject(TestRunMqttService); + private focusManagerService = inject(FocusManagerService); + private notificationService = inject(NotificationService); + private consentShown$ = this.select(state => state.consentShown); + private calloutState$ = this.select(state => state.calloutState); private isStatusLoaded$ = this.select(state => state.isStatusLoaded); + private hasInternetConnection$ = this.store.select(selectInternetConnection); private hasDevices$ = this.store.select(selectHasDevices); + private isAllDevicesOutdated$ = this.store.select(selectIsAllDevicesOutdated); + private hasExpiredDevices$ = this.store.select(selectHasExpiredDevices); private hasRiskProfiles$ = this.store.select(selectHasRiskProfiles); + private reports$ = this.store.select(selectReports); private hasConnectionSetting$ = this.store.select( selectHasConnectionSettings ); - private isMenuOpen$ = this.store.select(selectMenuOpened); private interfaces$: Observable = this.store.select(selectInterfaces); + private systemConfig$: Observable = + this.store.select(selectSystemConfig); private settingMissedError$: Observable = - this.store.select(selectError); + this.select(state => state.settingMissedError); systemStatus$: Observable = this.store.select(selectStatus); + testrunStatus$: Observable = + this.store.select(selectSystemStatus); + isTestingComplete$: Observable = this.store.select( + selectIsTestingComplete + ); + riskProfiles$: Observable = this.store.select(selectRiskProfiles); + + testrunButtonDisabled$ = combineLatest([ + this.hasDevices$, + this.isAllDevicesOutdated$, + this.isStatusLoaded$, + this.systemStatus$, + this.hasConnectionSetting$, + ]).pipe( + map( + ([ + hasDevices, + isAllDevicesOutdated, + isStatusLoaded, + systemStatus, + hasConnectionSettings, + ]) => { + return !( + hasConnectionSettings === true && + hasDevices && + (!systemStatus || + !this.testRunService.testrunInProgress(systemStatus)) && + isStatusLoaded === true && + !isAllDevicesOutdated + ); + } + ) + ); viewModel$ = this.select({ consentShown: this.consentShown$, hasDevices: this.hasDevices$, + isAllDevicesOutdated: this.isAllDevicesOutdated$, + hasExpiredDevices: this.hasExpiredDevices$, hasRiskProfiles: this.hasRiskProfiles$, + reports: this.reports$, isStatusLoaded: this.isStatusLoaded$, systemStatus: this.systemStatus$, + testrunStatus: this.testrunStatus$, + isTestingComplete: this.isTestingComplete$, + riskProfiles: this.riskProfiles$, hasConnectionSettings: this.hasConnectionSetting$, - isMenuOpen: this.isMenuOpen$, interfaces: this.interfaces$, settingMissedError: this.settingMissedError$, + calloutState: this.calloutState$, + hasInternetConnection: this.hasInternetConnection$, }); updateConsent = this.updater((state, consentShown: boolean) => ({ @@ -80,11 +162,29 @@ export class AppStore extends ComponentStore { consentShown, })); + updateCalloutState = this.updater((state, callout: string) => { + const calloutState = state.calloutState; + calloutState.set(callout, true); + // @ts-expect-error property is defined in index.html + sessionStorage.setObject(CALLOUT_STATE_KEY, calloutState); + return { + ...state, + calloutState: new Map(calloutState), + }; + }); + updateIsStatusLoaded = this.updater((state, isStatusLoaded: boolean) => ({ ...state, isStatusLoaded, })); + updateSettingMissedError = this.updater( + (state, settingMissedError: SettingMissedError | null) => ({ + ...state, + settingMissedError, + }) + ); + setContent = this.effect(trigger$ => { return trigger$.pipe( tap(() => { @@ -131,6 +231,27 @@ export class AppStore extends ComponentStore { ); }); + getNetworkAdapters = this.effect(trigger$ => { + return trigger$.pipe( + exhaustMap(() => { + return this.testRunMqttService.getNetworkAdapters().pipe( + tap((adapters: Adapters) => { + if (adapters.adapters_added) { + this.notifyAboutTheAdapters(adapters.adapters_added); + } + this.store.dispatch(updateAdapters({ adapters })); + }) + ); + }) + ); + }); + + private notifyAboutTheAdapters(adapters: SystemInterfaces) { + this.notificationService.notify( + `New network adapter(s) ${Object.keys(adapters).join(', ')} has been detected. You can switch to using it in the System settings menu` + ); + } + setIsOpenStartTestrun = this.effect(trigger$ => { return trigger$.pipe( tap(() => { @@ -141,24 +262,124 @@ export class AppStore extends ComponentStore { ); }); - setFocusOnPage = this.effect(trigger$ => { + setFocusOnPage = this.effect( + trigger$ => { + return trigger$.pipe( + delay(100), + tap(element => { + this.focusManagerService.focusFirstElementInContainer(element); + }) + ); + } + ); + + getReports = this.effect(trigger$ => { return trigger$.pipe( - delay(100), tap(() => { - this.focusManagerService.focusFirstElementInContainer(); + this.store.dispatch(fetchReports()); }) ); }); - constructor( - private store: Store, - private testRunService: TestRunService, - private focusManagerService: FocusManagerService - ) { + getTestModules = this.effect(trigger$ => { + return trigger$.pipe( + exhaustMap(() => { + return this.testRunService.getTestModules().pipe( + tap((testModules: string[]) => { + this.store.dispatch( + setTestModules({ + testModules: testModules.map( + module => + ({ + displayName: module, + name: module.toLowerCase(), + enabled: true, + }) as TestModule + ), + }) + ); + }) + ); + }) + ); + }); + + setCloseCallout = this.effect(trigger$ => { + return trigger$.pipe( + tap((id: string) => { + this.updateCalloutState(id); + }) + ); + }); + + checkInterfacesInConfig = this.effect(() => { + return combineLatest([ + this.interfaces$.pipe(skip(1)), + this.systemConfig$.pipe(skip(1)), + ]).pipe( + filter(([, { network }]) => network !== null), + tap(([interfaces, { network, single_intf }]) => { + const deviceValid = + network?.device_intf == '' || + (!!network?.device_intf && !!interfaces[network.device_intf]); + const internetValid = single_intf + ? true + : network?.internet_intf == '' || + (!!network?.internet_intf && !!interfaces[network.internet_intf]); + this.updateSettingMissedError({ + isSettingMissed: !deviceValid || !internetValid, + devicePortMissed: !deviceValid, + internetPortMissed: !internetValid, + }); + }) + ); + }); + + sendGAEvent = this.effect(() => { + return combineLatest([this.isTestingComplete$, this.testrunStatus$]).pipe( + filter(([isTestingComplete]) => isTestingComplete === true), + filter( + ([, testrunStatus]) => + testrunStatus?.result === ResultOfTestrun.Compliant && + testrunStatus?.device.test_pack === TestingType.Pilot + ), + tap(() => { + // @ts-expect-error data layer is not null + window.dataLayer.push({ + event: 'pilot_is_compliant', + }); + }) + ); + }); + + getInterfaces = this.effect(trigger$ => { + return trigger$.pipe( + tap(() => { + this.store.dispatch(fetchInterfaces()); + }) + ); + }); + + getSystemConfig = this.effect(trigger$ => { + return trigger$.pipe( + tap(() => { + this.store.dispatch(fetchSystemConfig()); + }) + ); + }); + + constructor() { + // @ts-expect-error get object is defined in index.html + const calloutState = sessionStorage.getObject(CALLOUT_STATE_KEY); + super({ consentShown: sessionStorage.getItem(CONSENT_SHOWN_KEY) !== null, isStatusLoaded: false, systemStatus: null, + calloutState: calloutState + ? new Map(Object.entries(calloutState)) + : new Map(), + settingMissedError: null, }); } } diff --git a/modules/ui/src/app/components/bypass/bypass.component.spec.ts b/modules/ui/src/app/components/bypass/bypass.component.spec.ts index 8ba531e7e..a69229d8a 100644 --- a/modules/ui/src/app/components/bypass/bypass.component.spec.ts +++ b/modules/ui/src/app/components/bypass/bypass.component.spec.ts @@ -25,6 +25,7 @@ import SpyObj = jasmine.SpyObj; template: '' + '
', + standalone: false, }) class TestBypassComponent {} diff --git a/modules/ui/src/app/components/bypass/bypass.component.ts b/modules/ui/src/app/components/bypass/bypass.component.ts index 3a7d2819c..08f2363f5 100644 --- a/modules/ui/src/app/components/bypass/bypass.component.ts +++ b/modules/ui/src/app/components/bypass/bypass.component.ts @@ -13,20 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { FocusManagerService } from '../../services/focus-manager.service'; @Component({ selector: 'app-bypass', - standalone: true, + imports: [CommonModule, MatButtonModule], templateUrl: './bypass.component.html', styleUrls: ['./bypass.component.scss'], }) export class BypassComponent { - constructor(private readonly focusManagerService: FocusManagerService) {} + private readonly focusManagerService = inject(FocusManagerService); + skipToMainContent(event: Event) { event.preventDefault(); this.focusManagerService.focusFirstElementInContainer(); diff --git a/modules/ui/src/app/components/callout/callout.component.html b/modules/ui/src/app/components/callout/callout.component.html index a3fb7930c..918dc0c16 100644 --- a/modules/ui/src/app/components/callout/callout.component.html +++ b/modules/ui/src/app/components/callout/callout.component.html @@ -13,14 +13,39 @@ See the License for the specific language governing permissions and limitations under the License. --> -
+
- {{ type }} + fontSet="material-symbols-outlined"> + {{ type() }} + +

+
+ {{ action() }} + +
+
diff --git a/modules/ui/src/app/components/callout/callout.component.scss b/modules/ui/src/app/components/callout/callout.component.scss index 8a9d77125..994e6c580 100644 --- a/modules/ui/src/app/components/callout/callout.component.scss +++ b/modules/ui/src/app/components/callout/callout.component.scss @@ -14,106 +14,108 @@ * limitations under the License. */ @use '@angular/material' as mat; -@import '../../../theming/colors'; -@import '../../../theming/variables'; +@use 'm3-theme' as *; +@use 'colors'; +@use 'variables'; :host { width: 100%; } -:host:has(.callout-container.info), -:host:has(.callout-container.error), -:host:has(.callout-container.check_circle) { - position: absolute; +:host .info-pilot ::ng-deep app-program-type-icon { + padding-right: 1px; + + .icon { + width: 18px; + height: 18px; + line-height: 18px; + } } -:host + ::ng-deep app-callout { - top: 60px; +.check_circle { + color: colors.$on-tertiary-container; + background-color: rgba(20, 108, 46, 0.1); } -@media (width < 742px) { - :host + ::ng-deep app-callout { - top: 80px; - } +.info { + color: colors.$primary; + background-color: rgba(127, 207, 255, 0.16); } -@media (width < 490px) { - :host + ::ng-deep app-callout { - top: 100px; - } +.error { + color: colors.$on-error-container; + background-color: rgba(179, 38, 30, 0.1); +} + +.warning { + color: colors.$orange-40; + background-color: colors.$orange-98; +} + +.warning .callout-border { + color: colors.$orange-60; } .callout-container { + position: relative; + overflow: hidden; display: flex; box-sizing: border-box; height: auto; min-height: 48px; - padding: 6px 24px; - border-radius: 8px; + padding: 6px 6px 6px 16px; + border-radius: variables.$corner-medium; align-items: center; gap: 16px; + margin: 24px 32px; } .callout-icon { flex-shrink: 0; padding: 4px 0; + color: inherit; } -.callout-container.info { - margin: 24px 32px; - background-color: mat.get-color-from-palette($color-primary, 50); - - .callout-icon { - color: mat.get-color-from-palette($color-primary, 700); - } +.callout-context { + margin: 0; + padding: 6px 0; + color: colors.$on-surface-variant; + font-family: variables.$font-secondary; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; } -.callout-container.warning_amber { - background-color: $yellow-50; - - .callout-icon { - color: $orange-700; - } +.callout-action-container { + display: flex; + margin-left: auto; } -.callout-container.error { - margin: 24px 32px; - background-color: $red-50; - - .callout-icon { - color: $red-700; +@media (max-width: 800px) { + .callout-action-container { + flex-direction: column; } } -.callout-container.check_circle { - margin: 24px 32px; - background-color: $green-50; - - .callout-icon { - color: $green-800; - } +.callout-action { + color: inherit; + padding: 10px 16px; } -.callout-container.error_outline { - display: flex; - align-items: flex-start; - background: $color-background-grey; - - .callout-icon { - color: $grey-700; - } - - .callout-context { - font-weight: bold; - } +.callout-action-link { + text-decoration: none; + cursor: pointer; + font-weight: 500; + font-size: var(--mdc-text-button-label-text-size); + font-family: var(--mdc-text-button-label-text-font); } -.callout-context { - margin: 0; - padding: 6px 0; - color: $grey-800; - font-family: $font-secondary; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.2px; +.callout-border { + display: block; + height: 3px; + border-bottom: 3px solid; + width: 100%; + position: absolute; + bottom: 0; + margin-left: -16px; } diff --git a/modules/ui/src/app/components/callout/callout.component.spec.ts b/modules/ui/src/app/components/callout/callout.component.spec.ts index 28215ec7c..7b6a98cd8 100644 --- a/modules/ui/src/app/components/callout/callout.component.spec.ts +++ b/modules/ui/src/app/components/callout/callout.component.spec.ts @@ -16,6 +16,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CalloutComponent } from './callout.component'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; describe('CalloutComponent', () => { let component: CalloutComponent; @@ -24,11 +25,11 @@ describe('CalloutComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [CalloutComponent], + imports: [CalloutComponent, MatIconTestingModule], }).compileComponents(); fixture = TestBed.createComponent(CalloutComponent); component = fixture.componentInstance; - component.type = 'mockValue'; + fixture.componentRef.setInput('type', 'mockValue'); compiled = fixture.nativeElement as HTMLElement; fixture.detectChanges(); }); @@ -42,4 +43,50 @@ describe('CalloutComponent', () => { expect(calloutContainerdEl?.classList).toContain('mockValue'); }); + + describe('closeable', () => { + beforeEach(() => { + fixture.componentRef.setInput('closable', true); + fixture.detectChanges(); + }); + + it('should have close button', () => { + const closeButton = compiled.querySelector('.callout-close-button'); + + expect(closeButton).toBeTruthy(); + }); + + it('should emit event', () => { + const calloutClosedSpy = spyOn(component.calloutClosed, 'emit'); + const closeButton = compiled.querySelector( + '.callout-close-button' + ) as HTMLButtonElement; + closeButton?.click(); + + expect(calloutClosedSpy).toHaveBeenCalled(); + }); + }); + + describe('action', () => { + beforeEach(() => { + fixture.componentRef.setInput('action', 'action'); + fixture.detectChanges(); + }); + + it('should have action link', () => { + const closeButton = compiled.querySelector('.callout-action-link'); + + expect(closeButton).toBeTruthy(); + }); + + it('should emit event', () => { + const calloutClosedSpy = spyOn(component.onAction, 'emit'); + const actionLink = compiled.querySelector( + '.callout-action-link' + ) as HTMLAnchorElement; + actionLink?.click(); + + expect(calloutClosedSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/modules/ui/src/app/components/callout/callout.component.ts b/modules/ui/src/app/components/callout/callout.component.ts index bfb6ea9bf..2081f9f22 100644 --- a/modules/ui/src/app/components/callout/callout.component.ts +++ b/modules/ui/src/app/components/callout/callout.component.ts @@ -13,18 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + input, + output, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { CalloutType } from '../../model/callout-type'; +import { ProgramType } from '../../model/program-type'; +import { ProgramTypeIconComponent } from '../program-type-icon/program-type-icon.component'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + MatIconModule, + MatButtonModule, + ProgramTypeIconComponent, + ], selector: 'app-callout', - standalone: true, - imports: [CommonModule, MatIconModule], - templateUrl: './callout.component.html', + styleUrls: ['./callout.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './callout.component.html', }) export class CalloutComponent { - @Input() type = ''; + readonly CalloutType = CalloutType; + readonly ProgramType = ProgramType; + id = input(null); + type = input(''); + closable = input(false); + action = input(); + calloutClosed = output(); + onAction = output(); } diff --git a/modules/ui/src/app/components/component-with-announcement.ts b/modules/ui/src/app/components/component-with-announcement.ts new file mode 100644 index 000000000..0f555e15a --- /dev/null +++ b/modules/ui/src/app/components/component-with-announcement.ts @@ -0,0 +1,35 @@ +import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { FocusManagerService } from '../services/focus-manager.service'; +import { timer } from 'rxjs'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Constructor = new (...args: any[]) => T; + +export function ComponentWithAnnouncement(base: T) { + return class extends base { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(...args: any[]) { + const [dialogRef, title, liveAnnouncer, focusService] = args; + super(...args); + + this.focusService = focusService; + + dialogRef.afterOpened().subscribe(() => { + (liveAnnouncer as LiveAnnouncer).clear(); + (liveAnnouncer as LiveAnnouncer) + .announce(title, 'assertive') + .then(() => { + timer(200).subscribe(() => { + this.focusService.focusFirstElementInContainer(); + }); + }); + }); + + dialogRef.beforeClosed().subscribe(() => { + (liveAnnouncer as LiveAnnouncer).clear(); + }); + } + + focusService!: FocusManagerService; + }; +} diff --git a/modules/ui/src/app/components/device-item/device-item.component.html b/modules/ui/src/app/components/device-item/device-item.component.html index 8f85b6edf..7b728b33c 100644 --- a/modules/ui/src/app/components/device-item/device-item.component.html +++ b/modules/ui/src/app/components/device-item/device-item.component.html @@ -14,56 +14,81 @@ limitations under the License. -->
+ +
+ +
+ [class.device-item-outdated]="device.status === DeviceStatus.INVALID"> -
+ + + {{ device.test_pack }} + + +

+ {{ device.manufacturer }} +

+ + error + +
+

{{ device.manufacturer }}

+ + edit_square +
+
+ {{ INVALID_DEVICE }} +
+
+ Under test +
+

+ {{ device.model }} +

+
diff --git a/modules/ui/src/app/components/device-item/device-item.component.scss b/modules/ui/src/app/components/device-item/device-item.component.scss index 2f0c65b14..7ea530c0b 100644 --- a/modules/ui/src/app/components/device-item/device-item.component.scss +++ b/modules/ui/src/app/components/device-item/device-item.component.scss @@ -14,110 +14,145 @@ * limitations under the License. */ @use '@angular/material' as mat; -@import '../../../theming/colors'; -@import '../../../theming/variables'; +@use 'm3-theme' as *; +@use 'colors'; +@use 'variables'; +@use 'mixins'; $icon-width: 80px; $border-radius: 12px; .device-item { display: grid; - width: $device-item-width; - height: 80px; - border-radius: $border-radius; - border: 1px solid #c4c7c5; - background: $white; + grid-column-gap: 24px; box-sizing: border-box; - grid-template-columns: 1fr 1fr; - padding: 0; - grid-column-gap: 8px; - grid-row-gap: 4px; - font-family: 'Open Sans', sans-serif; + font-family: variables.$font-primary; + border-radius: variables.$corner-large; + align-items: center; + border: none; +} + +.device-item-basic { + padding: 0 24px 0 32px; + width: variables.$device-item-width; + height: 92px; + box-shadow: + 0px 1px 2px 0px rgba(0, 0, 0, 0.3), + 0px 1px 3px 1px rgba(0, 0, 0, 0.15); + background: colors.$surface-container; + grid-template-columns: auto 1fr; grid-template-areas: - 'manufacturer manufacturer' - 'name address'; + 'icon manufacturer' + 'icon name'; &:hover { cursor: pointer; + background: colors.$primary-container; } - &.non-interective { + &.non-interactive { + background: colors.$primary-container; &:hover { cursor: default; } } + + .item-manufacturer { + display: block; + max-width: 100%; + box-sizing: border-box; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + margin: 0; + } + + .item-name { + width: 230px; + box-sizing: border-box; + text-align: start; + } + + .item-mac-address { + margin: 0; + } } -.device-item-with-actions { - display: grid; - width: $device-item-width; - min-height: calc($icon-width - 2px); - border-radius: $border-radius; - border: 1px solid #c4c7c5; - background: $white; - box-sizing: border-box; - grid-template-columns: 1fr $icon-width; - grid-column-gap: 1px; - padding: 0; - font-family: 'Open Sans', sans-serif; - grid-template-areas: 'edit start'; +.button-edit:has(.item-status) { + grid-template-columns: min-content auto min-content; + grid-template-areas: + 'icon manufacturer status' + 'icon name name'; } .button-edit { - display: grid; - grid-area: edit; - background: $white; - box-sizing: border-box; - grid-template-columns: 1fr 1fr; - padding: 0 6px 0 0; - grid-column-gap: 8px; - grid-row-gap: 4px; - font-family: 'Open Sans', sans-serif; + width: 100%; + height: 100%; + background: inherit; + grid-template-columns: min-content auto; grid-template-areas: - 'manufacturer manufacturer' - 'name address'; - border-radius: $border-radius 0 0 $border-radius; - border: none; + 'icon manufacturer' + 'icon name'; &:hover { cursor: pointer; - .item-manufacturer-text { - max-width: 206px; - } - .item-manufacturer-icon { + position: absolute; + right: 0; visibility: visible; width: 24px; } } &:disabled { - pointer-events: none; - opacity: 0.5; + @include mixins.disabled; + } +} + +.item-status { + align-self: end; + white-space: nowrap; + grid-area: status; + justify-self: start; + font-size: 11px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.64px; + text-transform: uppercase; + max-width: 100%; + border-radius: 200px; + padding: 0 7px; + &-under-test { + background: colors.$on-secondary-container; + color: colors.$on-secondary; + } + &-invalid { + background: colors.$error-container; + color: colors.$on-error-container; } } .item-manufacturer { display: flex; - padding: 0 16px; grid-area: manufacturer; justify-self: start; align-self: end; - color: #1f1f1f; + color: colors.$on-surface; justify-content: flex-start; font-size: 16px; font-weight: 500; line-height: 24px; text-align: start; - gap: 4px; + position: relative; + padding-right: variables.$icon-size; .item-manufacturer-text { + max-width: 240px; margin: 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; - max-width: 230px; } .item-manufacturer-icon { @@ -131,10 +166,10 @@ $border-radius: 12px; } .item-name { - padding: 0 2px 0 16px; + align-self: start; grid-area: name; justify-self: start; - color: $grey-800; + color: colors.$on-surface-variant; font-size: 14px; font-style: normal; font-weight: 400; @@ -146,61 +181,22 @@ $border-radius: 12px; max-width: -webkit-fill-available; max-width: -moz-available; text-align: left; + margin: 0; } -.item-mac-address { - padding-right: 16px; - grid-area: address; - justify-self: end; - color: $grey-700; - font-family: Roboto, sans-serif; - font-size: 12px; - padding-top: 2px; - line-height: 20px; - max-width: 100%; +app-program-type-icon, +mat-icon { + grid-area: icon; } -.button-start { - grid-area: start; - width: $icon-width; +.device-item-outdated { height: 100%; - background-color: mat.get-color-from-palette($color-primary, 50); - justify-self: end; - border-radius: 0 $border-radius $border-radius 0; - - &:hover, - &:focus-visible { - background-color: mat.get-color-from-palette($color-primary, 600); - - .button-start-icon { - color: $white; - } - } - &:disabled { - pointer-events: none; - opacity: 0.5; - } -} - -.button-start-icon { - margin: 0; - width: 30px; - height: 24px; -} - -.device-item { - .item-manufacturer { - display: block; - max-width: 100%; - box-sizing: border-box; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; + .item-manufacturer, + .item-name, + mat-icon { + color: colors.$on-error-container; } - - .item-name { - width: 230px; - box-sizing: border-box; - text-align: start; + .item-manufacturer-text { + max-width: 150px; } } diff --git a/modules/ui/src/app/components/device-item/device-item.component.spec.ts b/modules/ui/src/app/components/device-item/device-item.component.spec.ts index 2fe79d8db..350fa7548 100644 --- a/modules/ui/src/app/components/device-item/device-item.component.spec.ts +++ b/modules/ui/src/app/components/device-item/device-item.component.spec.ts @@ -14,10 +14,14 @@ * limitations under the License. */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Device, DeviceView } from '../../model/device'; +import { + Device, + DeviceStatus, + DeviceView, + TestingType, +} from '../../model/device'; import { DeviceItemComponent } from './device-item.component'; -import { DevicesModule } from '../../pages/devices/devices.module'; import { MatIconTestingModule } from '@angular/material/icon/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -29,7 +33,6 @@ describe('DeviceItemComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ - DevicesModule, DeviceItemComponent, MatIconTestingModule, BrowserAnimationsModule, @@ -39,6 +42,7 @@ describe('DeviceItemComponent', () => { component = fixture.componentInstance; compiled = fixture.nativeElement as HTMLElement; component.device = { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: 'O3-DIN-CPU', mac_addr: '00:1e:42:35:73:c4', @@ -64,27 +68,35 @@ describe('DeviceItemComponent', () => { it('should display information about device', () => { const name = compiled.querySelector('.item-name'); const manufacturer = compiled.querySelector('.item-manufacturer'); - const mac = compiled.querySelector('.item-mac-address'); expect(name?.textContent?.trim()).toEqual('O3-DIN-CPU'); expect(manufacturer?.textContent?.trim()).toEqual('Delta'); - expect(mac?.textContent?.trim()).toEqual('00:1e:42:35:73:c4'); }); - it('should emit mac address', () => { - const clickSpy = spyOn(component.itemClicked, 'emit'); - const item = compiled.querySelector('.device-item') as HTMLElement; - item.click(); + it('should have qualification icon if testing type is qualification', () => { + component.device.test_pack = TestingType.Qualification; + fixture.detectChanges(); + const icon = compiled.querySelector('app-program-type-icon'); - expect(clickSpy).toHaveBeenCalledWith(component.device); + expect(icon).toBeTruthy(); + expect(icon?.getAttribute('ng-reflect-type')).toEqual('qualification'); }); - it('should have tabindex', () => { - component.tabIndex = -2; + it('should have pilot icon if testing type is pilot', () => { + component.device.test_pack = TestingType.Pilot; fixture.detectChanges(); + const icon = compiled.querySelector('app-program-type-icon'); + + expect(icon).toBeTruthy(); + expect(icon?.getAttribute('ng-reflect-type')).toEqual('pilot'); + }); + + it('should emit mac address', () => { + const clickSpy = spyOn(component.itemClicked, 'emit'); const item = compiled.querySelector('.device-item') as HTMLElement; + item.click(); - expect(item.tabIndex).toBe(-2); + expect(clickSpy).toHaveBeenCalledWith(component.device); }); }); @@ -94,17 +106,40 @@ describe('DeviceItemComponent', () => { fixture.detectChanges(); }); - it('should emit device on click edit button', () => { - const clickSpy = spyOn(component.itemClicked, 'emit'); - const editBtn = compiled.querySelector('.button-edit') as HTMLElement; - editBtn.click(); + describe('with device status as invalid', () => { + beforeEach(() => { + component.device.status = DeviceStatus.INVALID; + fixture.detectChanges(); + }); - expect(clickSpy).toHaveBeenCalledWith(component.device); + it('should have item status as Outdated', () => { + component.device.status = DeviceStatus.INVALID; + fixture.detectChanges(); + const status = compiled.querySelector('.item-status'); + + expect(status).toBeTruthy(); + expect(status?.textContent?.trim()).toEqual('Outdated'); + }); + + it('should have error icon', () => { + const icon = compiled.querySelector('mat-icon')?.textContent?.trim(); + + expect(icon).toEqual('error'); + }); + }); + + it('should have item status as Under test', () => { + component.disabled = true; + fixture.detectChanges(); + const status = compiled.querySelector('.item-status'); + + expect(status).toBeTruthy(); + expect(status?.textContent?.trim()).toEqual('Under test'); }); - it('should emit device on click start button', () => { - const clickSpy = spyOn(component.startTestrunClicked, 'emit'); - const editBtn = compiled.querySelector('.button-start') as HTMLElement; + it('should emit device on click edit button', () => { + const clickSpy = spyOn(component.itemClicked, 'emit'); + const editBtn = compiled.querySelector('.button-edit') as HTMLElement; editBtn.click(); expect(clickSpy).toHaveBeenCalledWith(component.device); @@ -114,22 +149,18 @@ describe('DeviceItemComponent', () => { component.disabled = true; fixture.detectChanges(); - const startBtn = compiled.querySelector('.button-start') as HTMLElement; const editBtn = compiled.querySelector('.button-edit') as HTMLElement; expect(editBtn.getAttribute('disabled')).not.toBeNull(); - expect(startBtn.getAttribute('disabled')).toBeTruthy(); }); it('should not disable buttons if disable set to false', () => { component.disabled = false; fixture.detectChanges(); - const startBtn = compiled.querySelector('.button-start') as HTMLElement; const editBtn = compiled.querySelector('.button-edit') as HTMLElement; expect(editBtn.getAttribute('disabled')).toBeNull(); - expect(startBtn.getAttribute('disabled')).toBeFalsy(); }); }); }); diff --git a/modules/ui/src/app/components/device-item/device-item.component.ts b/modules/ui/src/app/components/device-item/device-item.component.ts index bc59fcd16..d78dbfd6d 100644 --- a/modules/ui/src/app/components/device-item/device-item.component.ts +++ b/modules/ui/src/app/components/device-item/device-item.component.ts @@ -14,36 +14,51 @@ * limitations under the License. */ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { Device, DeviceView } from '../../model/device'; +import { + Device, + DeviceStatus, + DeviceView, + TestingType, +} from '../../model/device'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { ProgramTypeIconComponent } from '../program-type-icon/program-type-icon.component'; +import { ProgramType } from '../../model/program-type'; @Component({ selector: 'app-device-item', templateUrl: './device-item.component.html', styleUrls: ['./device-item.component.scss'], - standalone: true, - imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule], + + imports: [ + CommonModule, + MatButtonModule, + MatIconModule, + MatTooltipModule, + ProgramTypeIconComponent, + ], }) export class DeviceItemComponent { + readonly DeviceStatus = DeviceStatus; + readonly TestingType = TestingType; + readonly ProgramType = ProgramType; + readonly INVALID_DEVICE = 'Outdated'; @Input() device!: Device; @Input() tabIndex = 0; @Input() deviceView!: string; @Input() disabled = false; @Output() itemClicked = new EventEmitter(); - @Output() startTestrunClicked = new EventEmitter(); readonly DeviceView = DeviceView; itemClick(): void { this.itemClicked.emit(this.device); } - startTestrunClick(): void { - this.startTestrunClicked.emit(this.device); - } get label() { - return `${this.device.manufacturer} ${this.device.model} ${this.device.mac_addr}`; + const deviceStatus = + this.device.status === DeviceStatus.INVALID ? this.INVALID_DEVICE : ''; + return `${this.device.test_pack} ${this.device.manufacturer} ${this.device.model} ${deviceStatus} ${this.device.mac_addr}`; } } diff --git a/modules/ui/src/app/components/device-tests/device-tests.component.scss b/modules/ui/src/app/components/device-tests/device-tests.component.scss index e7529f269..42bd1646d 100644 --- a/modules/ui/src/app/components/device-tests/device-tests.component.scss +++ b/modules/ui/src/app/components/device-tests/device-tests.component.scss @@ -13,12 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../theming/colors'; -@import '../../../theming/variables'; - -:host { - overflow: hidden; -} +@use 'colors'; +@use 'variables'; +@use 'mixins'; .device-tests-form { height: 100%; @@ -27,26 +24,21 @@ } .disabled { - pointer-events: none; - opacity: 0.6; + @include mixins.disabled; } .device-tests-title { margin: 20px 0 8px; font-size: 18px; line-height: 24px; - color: $grey-800; + color: colors.$on-surface-variant; } .device-tests-description { margin: 0; - font-family: $font-secondary; + font-family: variables.$font-text; font-size: 14px; line-height: 20px; letter-spacing: 0.2px; - color: $grey-800; -} - -.device-form-test-modules { - overflow: auto; + color: colors.$on-surface-variant; } diff --git a/modules/ui/src/app/components/device-tests/device-tests.component.spec.ts b/modules/ui/src/app/components/device-tests/device-tests.component.spec.ts index 0599af372..b959bc1db 100644 --- a/modules/ui/src/app/components/device-tests/device-tests.component.spec.ts +++ b/modules/ui/src/app/components/device-tests/device-tests.component.spec.ts @@ -65,14 +65,14 @@ describe('DeviceTestsComponent', () => { }); it('should fill tests with device test values if device not present', () => { - component.deviceTestModules = { + fixture.componentRef.setInput('deviceTestModules', { connection: { enabled: false, }, dns: { enabled: true, }, - }; + }); component.ngOnInit(); expect(component.test_modules.controls[0].value).toEqual(false); diff --git a/modules/ui/src/app/components/device-tests/device-tests.component.ts b/modules/ui/src/app/components/device-tests/device-tests.component.ts index 58e1a3c4d..a0df852e4 100644 --- a/modules/ui/src/app/components/device-tests/device-tests.component.ts +++ b/modules/ui/src/app/components/device-tests/device-tests.component.ts @@ -16,6 +16,8 @@ import { ChangeDetectionStrategy, Component, + effect, + input, Input, OnInit, } from '@angular/core'; @@ -31,7 +33,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox'; @Component({ selector: 'app-device-tests', - standalone: true, + imports: [CommonModule, MatCheckboxModule, ReactiveFormsModule], templateUrl: './device-tests.component.html', styleUrls: ['./device-tests.component.scss'], @@ -39,11 +41,15 @@ import { MatCheckboxModule } from '@angular/material/checkbox'; }) export class DeviceTestsComponent implements OnInit { @Input() deviceForm!: FormGroup; - @Input() deviceTestModules?: TestModules | null; @Input() testModules: TestModule[] = []; // For initiate test run form tests should be displayed and disabled for change @Input() disabled = false; + deviceTestModules = input(); + deviceTestModulesEffect = effect(() => { + this.fillTestModulesFormControls(); + }); + get test_modules() { return this.deviceForm?.controls['test_modules'] as FormArray; } @@ -54,11 +60,12 @@ export class DeviceTestsComponent implements OnInit { fillTestModulesFormControls() { this.test_modules.controls = []; - if (this.deviceTestModules) { + if (this.deviceTestModules()) { this.testModules.forEach(test => { this.test_modules.push( new FormControl( - (this.deviceTestModules as TestModules)[test.name]?.enabled || false + (this.deviceTestModules() as TestModules)[test.name]?.enabled || + false ) ); }); diff --git a/modules/ui/src/app/components/download-report-pdf/download-report-pdf.component.ts b/modules/ui/src/app/components/download-report-pdf/download-report-pdf.component.ts index 9262a11f2..bc68e1193 100644 --- a/modules/ui/src/app/components/download-report-pdf/download-report-pdf.component.ts +++ b/modules/ui/src/app/components/download-report-pdf/download-report-pdf.component.ts @@ -17,13 +17,12 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; import { DownloadReportComponent } from '../download-report/download-report.component'; import { ReportActionComponent } from '../report-action/report-action.component'; -import { MatIcon } from '@angular/material/icon'; @Component({ selector: 'app-download-report-pdf', templateUrl: './download-report-pdf.component.html', - standalone: true, - imports: [CommonModule, DownloadReportComponent, MatIcon], + + imports: [CommonModule, DownloadReportComponent], providers: [DatePipe], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts index d87200987..ae35a64bc 100644 --- a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts +++ b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.spec.ts @@ -13,20 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, -} from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; import { DownloadReportZipComponent } from './download-report-zip.component'; import { of } from 'rxjs'; import { MatDialogRef } from '@angular/material/dialog'; -import { DownloadZipModalComponent } from '../download-zip-modal/download-zip-modal.component'; -import { Router } from '@angular/router'; -import { TestRunService } from '../../services/test-run.service'; -import { Routes } from '../../model/routes'; +import { + DialogCloseAction, + DownloadZipModalComponent, +} from '../download-zip-modal/download-zip-modal.component'; import { RouterTestingModule } from '@angular/router/testing'; import { Component } from '@angular/core'; import { MOCK_PROGRESS_DATA_COMPLIANT } from '../../mocks/testrun.mock'; @@ -35,10 +30,6 @@ describe('DownloadReportZipComponent', () => { let component: DownloadReportZipComponent; let fixture: ComponentFixture; let compiled: HTMLElement; - let router: Router; - - const testrunServiceMock: jasmine.SpyObj = - jasmine.createSpyObj('testrunServiceMock', ['downloadZip']); beforeEach(async () => { await TestBed.configureTestingModule({ @@ -48,13 +39,12 @@ describe('DownloadReportZipComponent', () => { ]), DownloadReportZipComponent, ], - providers: [{ provide: TestRunService, useValue: testrunServiceMock }], }).compileComponents(); fixture = TestBed.createComponent(DownloadReportZipComponent); - router = TestBed.get(Router); compiled = fixture.nativeElement as HTMLElement; component = fixture.componentInstance; - component.url = 'localhost:8080'; + component.report = 'localhost:8080'; + component.export = 'localhost:8080'; component.data = MOCK_PROGRESS_DATA_COMPLIANT; }); @@ -64,82 +54,26 @@ describe('DownloadReportZipComponent', () => { }); describe('#onClick', () => { - beforeEach(() => { - testrunServiceMock.downloadZip.calls.reset(); - }); - - it('should call service if profile is a string', fakeAsync(() => { + it('should open zip modal dialog', fakeAsync(() => { const openSpy = spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(''), + afterClosed: () => + of({ action: DialogCloseAction.Download, profile: '' }), } as MatDialogRef); - component.onClick(new Event('click')); expect(openSpy).toHaveBeenCalledWith(DownloadZipModalComponent, { ariaLabel: 'Download zip', data: { profiles: [], + report: 'localhost:8080', + export: 'localhost:8080', + isPilot: false, }, autoFocus: true, hasBackdrop: true, disableClose: true, panelClass: 'initiate-test-run-dialog', }); - - tick(); - - expect(testrunServiceMock.downloadZip).toHaveBeenCalled(); - expect(router.url).not.toBe(Routes.RiskAssessment); - openSpy.calls.reset(); - })); - - it('should navigate to risk profiles page if profile is null', fakeAsync(() => { - const openSpy = spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(null), - } as MatDialogRef); - - fixture.ngZone?.run(() => { - component.onClick(new Event('click')); - - expect(openSpy).toHaveBeenCalledWith(DownloadZipModalComponent, { - ariaLabel: 'Download zip', - data: { - profiles: [], - }, - autoFocus: true, - hasBackdrop: true, - disableClose: true, - panelClass: 'initiate-test-run-dialog', - }); - - tick(); - - expect(router.url).toBe(Routes.RiskAssessment); - openSpy.calls.reset(); - }); - })); - - it('should do nothing if profile is undefined', fakeAsync(() => { - const openSpy = spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(undefined), - } as MatDialogRef); - - component.onClick(new Event('click')); - - expect(openSpy).toHaveBeenCalledWith(DownloadZipModalComponent, { - ariaLabel: 'Download zip', - data: { - profiles: [], - }, - autoFocus: true, - hasBackdrop: true, - disableClose: true, - panelClass: 'initiate-test-run-dialog', - }); - - tick(); - - expect(testrunServiceMock.downloadZip).not.toHaveBeenCalled(); openSpy.calls.reset(); })); }); @@ -171,9 +105,9 @@ describe('DownloadReportZipComponent', () => { expect(spyOnShow).toHaveBeenCalled(); }); - it('should be shown on focusin', () => { + it('should be shown on keyup', () => { const spyOnShow = spyOn(component.tooltip, 'show'); - fixture.nativeElement.dispatchEvent(new Event('focusin')); + fixture.nativeElement.dispatchEvent(new Event('keyup')); expect(spyOnShow).toHaveBeenCalled(); }); @@ -185,9 +119,9 @@ describe('DownloadReportZipComponent', () => { expect(spyOnHide).toHaveBeenCalled(); }); - it('should be hidden on focusout', () => { + it('should be hidden on keydown', () => { const spyOnHide = spyOn(component.tooltip, 'hide'); - fixture.nativeElement.dispatchEvent(new Event('focusout')); + fixture.nativeElement.dispatchEvent(new Event('keydown')); expect(spyOnHide).toHaveBeenCalled(); }); diff --git a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts index e7b106f05..8fa2645b5 100644 --- a/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts +++ b/modules/ui/src/app/components/download-report-zip/download-report-zip.component.ts @@ -19,36 +19,36 @@ import { HostBinding, HostListener, Input, - OnDestroy, OnInit, + inject, } from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; import { Profile } from '../../model/profile'; import { MatDialog } from '@angular/material/dialog'; -import { Subject, takeUntil } from 'rxjs'; -import { Routes } from '../../model/routes'; import { DownloadZipModalComponent } from '../download-zip-modal/download-zip-modal.component'; -import { TestRunService } from '../../services/test-run.service'; -import { Router } from '@angular/router'; import { ReportActionComponent } from '../report-action/report-action.component'; import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip'; +import { TestingType } from '../../model/device'; @Component({ selector: 'app-download-report-zip', templateUrl: './download-report-zip.component.html', styleUrl: './download-report-zip.component.scss', - standalone: true, + imports: [CommonModule, MatTooltipModule], providers: [DatePipe, MatTooltip], changeDetection: ChangeDetectionStrategy.OnPush, }) export class DownloadReportZipComponent extends ReportActionComponent - implements OnDestroy, OnInit + implements OnInit { - private destroy$: Subject = new Subject(); + dialog = inject(MatDialog); + tooltip = inject(MatTooltip); + @Input() profiles: Profile[] = []; - @Input() url: string | null | undefined = null; + @Input() report: string | null | undefined = null; + @Input() export: string | null | undefined = null; @HostListener('click', ['$event']) @HostListener('keydown.enter', ['$event']) @@ -57,69 +57,43 @@ export class DownloadReportZipComponent event.preventDefault(); event.stopPropagation(); - const dialogRef = this.dialog.open(DownloadZipModalComponent, { + this.dialog.open(DownloadZipModalComponent, { ariaLabel: 'Download zip', data: { profiles: this.profiles, + report: this.report, + export: this.export, + isPilot: this.data?.device.test_pack === TestingType.Pilot, }, autoFocus: true, hasBackdrop: true, disableClose: true, panelClass: 'initiate-test-run-dialog', }); - - dialogRef - ?.afterClosed() - .pipe(takeUntil(this.destroy$)) - .subscribe(profile => { - if (profile === undefined) { - return; - } - if (profile === null) { - this.route.navigate([Routes.RiskAssessment]); - } else if (this.url != null) { - this.testrunService.downloadZip(this.getZipLink(this.url), profile); - } - }); } @HostBinding('tabIndex') readonly tabIndex = 0; @HostListener('mouseenter') - @HostListener('focusin', ['$event']) + @HostListener('keyup', ['$event']) onEvent(): void { this.tooltip.show(); } @HostListener('mouseleave') - @HostListener('focusout', ['$event']) + @HostListener('keydown', ['$event']) outEvent(): void { this.tooltip.hide(); } - ngOnDestroy() { - this.destroy$.next(true); - this.destroy$.unsubscribe(); - } - ngOnInit() { if (this.data) { this.tooltip.message = `Download zip for Testrun # ${this.getTestRunId(this.data)}`; } } - constructor( - datePipe: DatePipe, - public dialog: MatDialog, - private testrunService: TestRunService, - private route: Router, - public tooltip: MatTooltip - ) { - super(datePipe); - } - - private getZipLink(reportURL: string): string { - return reportURL.replace('report', 'export'); + constructor() { + super(); } } diff --git a/modules/ui/src/app/components/download-report/download-report.component.html b/modules/ui/src/app/components/download-report/download-report.component.html index d4ee224fe..0e27377ed 100644 --- a/modules/ui/src/app/components/download-report/download-report.component.html +++ b/modules/ui/src/app/components/download-report/download-report.component.html @@ -17,7 +17,9 @@ diff --git a/modules/ui/src/app/components/download-report/download-report.component.scss b/modules/ui/src/app/components/download-report/download-report.component.scss index bf4f48926..211657355 100644 --- a/modules/ui/src/app/components/download-report/download-report.component.scss +++ b/modules/ui/src/app/components/download-report/download-report.component.scss @@ -13,15 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@use 'mixins'; + :host { display: inline-block; } .download-report-link { - display: flex; - align-items: center; - justify-content: center; - text-align: center; - padding: 4px 0; - margin: 0 4px; + @include mixins.report-action; } diff --git a/modules/ui/src/app/components/download-report/download-report.component.spec.ts b/modules/ui/src/app/components/download-report/download-report.component.spec.ts index f29430f54..af711a26b 100644 --- a/modules/ui/src/app/components/download-report/download-report.component.spec.ts +++ b/modules/ui/src/app/components/download-report/download-report.component.spec.ts @@ -21,6 +21,7 @@ import { MOCK_PROGRESS_DATA_COMPLIANT, MOCK_PROGRESS_DATA_NON_COMPLIANT, } from '../../mocks/testrun.mock'; +import { TestrunStatus } from '../../model/testrun-status'; describe('DownloadReportComponent', () => { let component: DownloadReportComponent; @@ -39,13 +40,26 @@ describe('DownloadReportComponent', () => { expect(component).toBeTruthy(); }); - it('#getReportTitle should return data for download property of link', () => { - const expectedResult = - 'delta_03-din-cpu_1.2.2_compliant_22_jun_2023_9:20'; + describe('#getReportTitle', () => { + it('should return data for download property of link', () => { + const expectedResult = + 'delta_03-din-cpu_1.2.2_compliant_22_jun_2023_9:20'; - const result = component.getReportTitle(MOCK_PROGRESS_DATA_COMPLIANT); + const result = component.getReportTitle(MOCK_PROGRESS_DATA_COMPLIANT); - expect(result).toEqual(expectedResult); + expect(result).toEqual(expectedResult); + }); + + it('should return empty string if no device data', () => { + const MOCK_DATA_WITHOUT_DEVICE = { + ...MOCK_PROGRESS_DATA_COMPLIANT, + device: undefined as unknown, + } as TestrunStatus; + + const result = component.getReportTitle(MOCK_DATA_WITHOUT_DEVICE); + + expect(result).toEqual(''); + }); }); describe('#getClass', () => { diff --git a/modules/ui/src/app/components/download-report/download-report.component.ts b/modules/ui/src/app/components/download-report/download-report.component.ts index 14d6c7659..7d2d52e3b 100644 --- a/modules/ui/src/app/components/download-report/download-report.component.ts +++ b/modules/ui/src/app/components/download-report/download-report.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { StatusOfTestrun, TestrunStatus } from '../../model/testrun-status'; +import { ResultOfTestrun, TestrunStatus } from '../../model/testrun-status'; import { CommonModule, DatePipe } from '@angular/common'; import { MatTooltipModule } from '@angular/material/tooltip'; import { ReportActionComponent } from '../report-action/report-action.component'; @@ -23,7 +23,7 @@ import { ReportActionComponent } from '../report-action/report-action.component' selector: 'app-download-report', templateUrl: './download-report.component.html', styleUrls: ['./download-report.component.scss'], - standalone: true, + imports: [CommonModule, MatTooltipModule], providers: [DatePipe], changeDetection: ChangeDetectionStrategy.OnPush, @@ -32,20 +32,24 @@ export class DownloadReportComponent extends ReportActionComponent { @Input() href: string | undefined; @Input() class!: string; @Input() title!: string; + @Input() tabindex = 0; getReportTitle(data: TestrunStatus) { + if (!data.device) { + return ''; + } return `${data.device.manufacturer} ${data.device.model} ${ data.device.firmware - } ${data.status} ${this.getFormattedDateString(data.started)}` + } ${data.result ? data.result : data.status} ${this.getFormattedDateString(data.started)}` .replace(/ /g, '_') .toLowerCase(); } getClass(data: TestrunStatus) { - if (data.status === StatusOfTestrun.Compliant) { + if (data.result === ResultOfTestrun.Compliant) { return `${this.class}-compliant`; } - if (data.status === StatusOfTestrun.NonCompliant) { + if (data.result === ResultOfTestrun.NonCompliant) { return `${this.class}-non-compliant`; } return this.class; diff --git a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.html b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.html index b3dfb77f4..61ba85614 100644 --- a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.html +++ b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.html @@ -13,22 +13,89 @@ See the License for the specific language governing permissions and limitations under the License. --> -Download ZIP file -

- Risk profile is required for device verification. Please, consider creating a - Risk assessment profile for your ZIP report. + +

+

+ {{ data.testrunStatus.device.manufacturer }} + {{ data.testrunStatus.device.model }} + {{ data.testrunStatus.device.firmware }} +

+

+ {{ data.testrunStatus.device.test_pack }} testing has just finished +

+
+
+

+ {{ getTestingResult(data.testrunStatus) }} +

+

+ {{ data.testrunStatus.description }} +

+
+ +Download ZIP file +

+ Risk Profile is required for device verification. Please consider going to + Risk Assessment + and creating a profile to attach to your report. +

+ +

+ Risk Profile is required for device verification. Please select a profile from + the list, or go to + Risk Assessment + and create a new one to attach to your report.

-
+
+ aria-label="Please choose a Risk Profile from the list"> - {{ selectedProfile }} + {{ selectedProfile.name }} + + {{ selectedProfile.risk }} risk + - +
- Please choose risk assessment profile + Please choose a Risk Profile from the list - - - - + + + + +
+ + +
- - - - - - diff --git a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.scss b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.scss index 3524cb936..aa4acbff3 100644 --- a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.scss +++ b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.scss @@ -13,15 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../theming/colors'; +@use '@angular/material' as mat; +@use 'colors'; +@use 'variables'; +@use 'mixins'; + +::ng-deep :root { + --mat-dialog-container-max-width: 570px; +} :host { - display: grid; - overflow: hidden; - box-sizing: border-box; - width: 570px; + @include mixins.dialog; padding: 24px 24px 16px 24px; gap: 10px; + overflow: auto; } .risk-profile-select-form-title { @@ -31,11 +36,12 @@ } .risk-profile-select-form-content { + margin: 14px 0 6px; font-family: Roboto, sans-serif; font-size: 14px; line-height: 20px; letter-spacing: 0.2px; - color: $grey-800; + color: colors.$grey-800; } .select-container { @@ -49,12 +55,23 @@ } .risk-profile-select-form-actions { + justify-content: flex-end; min-height: 30px; padding: 16px 0 0; -} + gap: 8px; + + &:has(app-download-report) { + justify-content: space-between; + } + + ::ng-deep .download-report-link { + width: fit-content; + text-decoration: none; -.risk-profile-select-form-actions button:first-child { - margin-right: auto; + &:hover:before { + content: none; + } + } } .profile-select { @@ -67,5 +84,102 @@ .profile-item-created { font-size: 12px; - color: $grey-700; + color: colors.$grey-700; +} + +.redirect-link { + cursor: pointer; + color: colors.$primary; + display: inline-block; + width: fit-content; +} + +::ng-deep mat-select-trigger { + display: inline-flex; + width: 100%; + justify-content: space-between; +} + +::ng-deep mat-select-trigger .profile-item-risk { + vertical-align: middle; + align-self: center; + margin-right: 16px; +} +.testing-result-heading { + margin: 16px 0; +} +.testing-result-title { + margin: 0; + font-size: 32px; + line-height: 40px; + text-align: center; + color: colors.$grey-900; +} + +.testing-result-subtitle { + margin: 0; + font-family: variables.$font-secondary; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + text-align: center; + color: colors.$grey-800; +} + +.testing-result { + display: flex; + height: auto; + min-height: 176px; + align-items: center; + gap: 8px; + margin: 6px 0 10px; + border-radius: 8px; +} + +.testing-result-status { + display: flex; + justify-content: center; + align-items: center; + flex: 1 0 0; + min-width: 208px; + width: fit-content; + height: 100%; + min-height: 176px; + box-sizing: border-box; + margin: 0; + padding: 16px; + border-radius: 8px; + color: colors.$white; + font-size: 24px; + line-height: 32px; + background: red; +} + +.testing-result-description { + display: flex; + justify-content: center; + box-sizing: border-box; + margin: 0; + padding: 8px 24px; + color: colors.$grey-800; + font-family: variables.$font-secondary; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; +} + +.failed-result { + background: colors.$red-50; + + .testing-result-status { + background: colors.$red-800; + } +} + +.success-result { + background: colors.$green-50; + + .testing-result-status { + background: #188038; /* TODO update with variable*/ + } } diff --git a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts index cdd4c665f..3f201b256 100644 --- a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts +++ b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts @@ -1,6 +1,15 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + discardPeriodicTasks, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; -import { DownloadZipModalComponent } from './download-zip-modal.component'; +import { + DialogCloseAction, + DownloadZipModalComponent, +} from './download-zip-modal.component'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { PROFILE_MOCK, @@ -10,31 +19,57 @@ import { import { of } from 'rxjs'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TestRunService } from '../../services/test-run.service'; +import { Routes } from '../../model/routes'; +import { Router } from '@angular/router'; +import { FocusManagerService } from '../../services/focus-manager.service'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Component } from '@angular/core'; describe('DownloadZipModalComponent', () => { + // @ts-expect-error data layer should be defined + window.dataLayer = window.dataLayer || []; let component: DownloadZipModalComponent; let fixture: ComponentFixture; - - const testRunServiceMock = jasmine.createSpyObj(['getRiskClass']); + let router: Router; + const testRunServiceMock = jasmine.createSpyObj('testRunServiceMock', [ + 'getRiskClass', + 'downloadZip', + ]); + const focusServiceMock: jasmine.SpyObj = + jasmine.createSpyObj('focusServiceMock', ['focusFirstElementInContainer']); + const actionBehaviorSubject$ = new BehaviorSubject({ + action: DialogCloseAction.Close, + }); beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DownloadZipModalComponent, NoopAnimationsModule], + imports: [ + RouterTestingModule.withRoutes([ + { path: 'risk-assessment', component: FakeRiskAssessmentComponent }, + ]), + DownloadZipModalComponent, + NoopAnimationsModule, + ], providers: [ { provide: MatDialogRef, useValue: { keydownEvents: () => of(new KeyboardEvent('keydown', { code: '' })), close: () => ({}), + beforeClosed: () => actionBehaviorSubject$.asObservable(), }, }, { provide: MAT_DIALOG_DATA, useValue: { profiles: [PROFILE_MOCK_2, PROFILE_MOCK], + report: 'localhost:8080', + export: 'localhost:8080', }, }, { provide: TestRunService, useValue: testRunServiceMock }, + { provide: FocusManagerService, useValue: focusServiceMock }, ], }); }); @@ -44,11 +79,15 @@ describe('DownloadZipModalComponent', () => { TestBed.overrideProvider(MAT_DIALOG_DATA, { useValue: { profiles: [PROFILE_MOCK_2, PROFILE_MOCK, PROFILE_MOCK_3], + report: 'localhost:8080', + export: 'localhost:8080', + isPilot: true, }, }); TestBed.compileComponents(); fixture = TestBed.createComponent(DownloadZipModalComponent); + router = TestBed.get(Router); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -59,43 +98,66 @@ describe('DownloadZipModalComponent', () => { expect(select).toBeTruthy(); }); - it('should preselect first profile', async () => { - const select = fixture.nativeElement.querySelector( - 'mat-select' - ) as HTMLElement; - - expect(select.getAttribute('ng-reflect-value')).toEqual( - 'Primary profile' + it('should preselect "no profile" option', async () => { + expect(component.selectedProfile.name).toEqual( + 'No Risk Profile selected' ); }); - it('should close with null on redirect button click', async () => { - const closeSpy = spyOn(component.dialogRef, 'close'); - const redirectButton = fixture.nativeElement.querySelector( - '.redirect-button' - ) as HTMLButtonElement; + it('should close with Redirect action on redirect button click', fakeAsync(() => { + const result = { + action: DialogCloseAction.Redirect, + }; + actionBehaviorSubject$.next(result); + fixture.detectChanges(); - redirectButton.click(); + fixture.ngZone?.run(() => { + const closeSpy = spyOn(component.dialogRef, 'close'); + const redirectLink = fixture.nativeElement.querySelector( + '.redirect-link' + ) as HTMLAnchorElement; - expect(closeSpy).toHaveBeenCalledWith(null); + redirectLink.click(); - closeSpy.calls.reset(); - }); + tick(2000); + + expect(router.url).toBe(Routes.RiskAssessment); + expect( + focusServiceMock.focusFirstElementInContainer + ).toHaveBeenCalled(); + expect(closeSpy).toHaveBeenCalledWith(result); + + closeSpy.calls.reset(); + discardPeriodicTasks(); + }); + })); - it('should close with undefined on cancel button click', async () => { + it('should close with Close action on cancel button click', async () => { + const result = { + action: DialogCloseAction.Close, + }; const closeSpy = spyOn(component.dialogRef, 'close'); + actionBehaviorSubject$.next(result); + fixture.detectChanges(); + const cancelButton = fixture.nativeElement.querySelector( '.cancel-button' ) as HTMLButtonElement; cancelButton.click(); - expect(closeSpy).toHaveBeenCalledWith(undefined); + expect(closeSpy).toHaveBeenCalledWith(result); closeSpy.calls.reset(); }); - it('should close with profile on download button click', async () => { + it('should close with Download action and profile on download button click', async () => { + const result = { + action: DialogCloseAction.Download, + profile: '', + }; + actionBehaviorSubject$.next(result); + fixture.detectChanges(); const closeSpy = spyOn(component.dialogRef, 'close'); const downloadButton = fixture.nativeElement.querySelector( '.download-button' @@ -103,13 +165,41 @@ describe('DownloadZipModalComponent', () => { downloadButton.click(); - expect(closeSpy).toHaveBeenCalledWith('Primary profile'); + expect(closeSpy).toHaveBeenCalledWith(result); + expect(testRunServiceMock.downloadZip).toHaveBeenCalled(); + expect(router.url).not.toBe(Routes.RiskAssessment); + closeSpy.calls.reset(); + }); + it('should send GA event if report is for Pilot program', async () => { + const result = { + action: DialogCloseAction.Download, + profile: '', + }; + actionBehaviorSubject$.next(result); + fixture.detectChanges(); + const closeSpy = spyOn(component.dialogRef, 'close'); + const downloadButton = fixture.nativeElement.querySelector( + '.download-button' + ) as HTMLButtonElement; + + downloadButton.click(); + + expect( + // @ts-expect-error data layer should be defined + window.dataLayer.some( + (item: { event: string }) => item.event === 'pilot_download_zip' + ) + ).toBeTruthy(); closeSpy.calls.reset(); }); it('should have filtered and sorted profiles', async () => { - expect(component.profiles).toEqual([PROFILE_MOCK, PROFILE_MOCK_2]); + expect(component.profiles).toEqual([ + component.NO_PROFILE, + PROFILE_MOCK, + PROFILE_MOCK_2, + ]); }); it('#getRiskClass should call the service method getRiskClass"', () => { @@ -132,35 +222,53 @@ describe('DownloadZipModalComponent', () => { TestBed.overrideProvider(MAT_DIALOG_DATA, { useValue: { profiles: [], + report: 'localhost:8080', + export: 'localhost:8080', }, }); TestBed.compileComponents(); fixture = TestBed.createComponent(DownloadZipModalComponent); + router = TestBed.get(Router); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should have no dropdown with profiles', async () => { + it('should have disabled dropdown', async () => { const select = fixture.nativeElement.querySelector('mat-select'); - expect(select).toEqual(null); + expect(select.classList.contains('mat-mdc-select-disabled')).toBeTruthy(); }); - it('should close with null on redirect button click', async () => { - const closeSpy = spyOn(component.dialogRef, 'close'); - const redirectButton = fixture.nativeElement.querySelector( - '.redirect-button' - ) as HTMLButtonElement; + it('should close with Redirect action on redirect button click', fakeAsync(() => { + const result = { + action: DialogCloseAction.Redirect, + }; + actionBehaviorSubject$.next(result); + fixture.detectChanges(); - redirectButton.click(); + fixture.ngZone?.run(() => { + const closeSpy = spyOn(component.dialogRef, 'close'); + const redirectLink = fixture.nativeElement.querySelector( + '.redirect-link' + ) as HTMLAnchorElement; - expect(closeSpy).toHaveBeenCalledWith(null); + redirectLink.click(); - closeSpy.calls.reset(); - }); + tick(2000); + + expect(router.url).toBe(Routes.RiskAssessment); + expect( + focusServiceMock.focusFirstElementInContainer + ).toHaveBeenCalled(); + expect(closeSpy).toHaveBeenCalledWith(result); - it('should close with undefined on cancel button click', async () => { + closeSpy.calls.reset(); + discardPeriodicTasks(); + }); + })); + + it('should close with Close action on cancel button click', async () => { const closeSpy = spyOn(component.dialogRef, 'close'); const cancelButton = fixture.nativeElement.querySelector( '.cancel-button' @@ -168,7 +276,9 @@ describe('DownloadZipModalComponent', () => { cancelButton.click(); - expect(closeSpy).toHaveBeenCalledWith(undefined); + expect(closeSpy).toHaveBeenCalledWith({ + action: DialogCloseAction.Close, + }); closeSpy.calls.reset(); }); @@ -181,9 +291,18 @@ describe('DownloadZipModalComponent', () => { downloadButton.click(); - expect(closeSpy).toHaveBeenCalledWith(''); + expect(closeSpy).toHaveBeenCalledWith({ + action: DialogCloseAction.Download, + profile: '', + }); closeSpy.calls.reset(); }); }); }); + +@Component({ + selector: 'app-fake-risk-assessment-component', + template: '', +}) +class FakeRiskAssessmentComponent {} diff --git a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts index 395bcb480..a4d10da2c 100644 --- a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts +++ b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogActions, @@ -17,14 +23,40 @@ import { MatFormField } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatOptionModule } from '@angular/material/core'; import { TestRunService } from '../../services/test-run.service'; +import { Routes } from '../../model/routes'; +import { Router } from '@angular/router'; +import { + TestrunStatus, + StatusOfTestrun, + ResultOfTestrun, +} from '../../model/testrun-status'; +import { DownloadReportComponent } from '../download-report/download-report.component'; +import { Subject, takeUntil, timer } from 'rxjs'; +import { FocusManagerService } from '../../services/focus-manager.service'; interface DialogData { profiles: Profile[]; + testrunStatus?: TestrunStatus; + isTestingComplete?: boolean; + report: string | null; + export: string | null; + isPilot?: boolean; +} + +export enum DialogCloseAction { + Close, + Redirect, + Download, +} + +export interface DialogCloseResult { + action: DialogCloseAction; + profile: string | null | undefined; } @Component({ selector: 'app-download-zip-modal', - standalone: true, + imports: [ CommonModule, MatDialogActions, @@ -35,20 +67,40 @@ interface DialogData { MatFormField, MatSelectModule, MatOptionModule, + DownloadReportComponent, ], templateUrl: './download-zip-modal.component.html', styleUrl: './download-zip-modal.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DownloadZipModalComponent extends EscapableDialogComponent { +export class DownloadZipModalComponent + extends EscapableDialogComponent + implements OnDestroy, OnInit +{ + private readonly testRunService = inject(TestRunService); + override dialogRef: MatDialogRef; + data = inject(MAT_DIALOG_DATA); + private route = inject(Router); + private focusManagerService = inject(FocusManagerService); + + private destroy$: Subject = new Subject(); + readonly NO_PROFILE = { + name: 'No Risk Profile selected', + questions: [], + } as Profile; + public readonly Routes = Routes; + public readonly StatusOfTestrun = StatusOfTestrun; + public readonly ResultOfTestrun = ResultOfTestrun; profiles: Profile[] = []; - selectedProfile: string = ''; - constructor( - private readonly testRunService: TestRunService, - public override dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: DialogData - ) { - super(dialogRef); + selectedProfile: Profile; + constructor() { + const dialogRef = + inject>(MatDialogRef); + + super(); + this.dialogRef = dialogRef; + const data = this.data; + this.profiles = data.profiles.filter( profile => profile.status === ProfileStatus.VALID ); @@ -56,15 +108,85 @@ export class DownloadZipModalComponent extends EscapableDialogComponent { this.profiles.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()) ); - this.selectedProfile = this.profiles[0].name; } + this.profiles.unshift(this.NO_PROFILE); + this.selectedProfile = this.profiles[0]; + } + + ngOnInit() { + this.dialogRef + ?.beforeClosed() + .pipe(takeUntil(this.destroy$)) + .subscribe((result: DialogCloseResult) => { + if (result.action === DialogCloseAction.Close) { + return; + } + if (result.action === DialogCloseAction.Redirect) { + this.route.navigate([Routes.RiskAssessment]).then(() => + timer(1000).subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(); + }) + ); + return; + } + if ( + (this.data.report != null || this.data.export != null) && + typeof result.profile === 'string' + ) { + this.testRunService.downloadZip( + this.getZipLink(this.data), + result.profile + ); + if (this.data.isPilot) { + // @ts-expect-error data layer is not null + window.dataLayer.push({ + event: 'pilot_download_zip', + }); + } + } + }); + } + + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); } - cancel(profile?: string | null) { - this.dialogRef.close(profile); + cancel(profile?: Profile | null) { + if (profile === null) { + this.dialogRef.close({ + action: DialogCloseAction.Redirect, + } as DialogCloseResult); + return; + } + if (!profile) { + this.dialogRef.close({ + action: DialogCloseAction.Close, + } as DialogCloseResult); + return; + } + let value = profile.name; + if (value === this.NO_PROFILE.name) { + value = ''; + } + this.dialogRef.close({ + action: DialogCloseAction.Download, + profile: value, + } as DialogCloseResult); } public getRiskClass(riskResult: string): RiskResultClassName { return this.testRunService.getRiskClass(riskResult); } + + public getTestingResult(data: TestrunStatus): string { + if (data.status === StatusOfTestrun.Complete && data.result) { + return data.result; + } + return data.status; + } + + private getZipLink(data: DialogData): string { + return data.export || data.report!.replace('report', 'export'); + } } diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html new file mode 100644 index 000000000..2a71722dd --- /dev/null +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ + description + }} + + Please, check. “ and \ are not allowed. + + + The field is required + + + The field must be a maximum of + {{ getControl(formControlName).getError('maxlength').requiredLength }} + characters. + + + + + + + + {{ + description + }} + + Please, check. “ and \ are not allowed. + + + The field is required + + + The field must be a maximum of + {{ getControl(formControlName).getError('maxlength').requiredLength }} + characters. + + + + + + + + {{ + description + }} + + The field is required + + + Please, check the email address. Valid e-mail can contain only latin + letters, numbers, @ and . (dot). + + + The field must be a maximum of + {{ getControl(formControlName).getError('maxlength').requiredLength }} + characters. + + + + + +
+

+ + + +

+ {{ + description + }} +
+
+ + + + + + {{ getControl(formControlName).value }} + + + + + {{ + description + }} + + + The field is required + + + + diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss new file mode 100644 index 000000000..3de387ce9 --- /dev/null +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss @@ -0,0 +1,71 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@use 'm3-theme' as *; +@use 'colors'; +@use 'variables'; + +.field-label { + font-family: variables.$font-text; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.1px; + color: colors.$on-surface-variant; + padding: 8px 16px; + display: inline-block; + &:has(+ .field-select-multiple.ng-invalid.ng-dirty) { + color: mat.get-theme-color($light-theme, error, 40); + } +} +mat-form-field { + width: 100%; +} +.field-hint { + font-family: variables.$font-secondary; + font-size: 12px; + font-weight: 400; + line-height: 16px; + text-align: left; +} + +.form-field { + width: 100%; +} + +.form-field ::ng-deep .mat-mdc-form-field-textarea-control { + display: inherit; +} + +.field-select-multiple { + margin-bottom: 16px; + + .field-select-checkbox { + &:has(::ng-deep .mat-mdc-checkbox-checked) { + background: mat.get-theme-color($light-theme, primary, 95); + } + ::ng-deep .mdc-checkbox__ripple { + display: none; + } + &:first-of-type { + margin-top: 0; + } + &:last-of-type { + margin-bottom: 8px; + } + } +} diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.spec.ts b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.spec.ts new file mode 100644 index 000000000..1754893f7 --- /dev/null +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.spec.ts @@ -0,0 +1,250 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DynamicFormComponent } from './dynamic-form.component'; +import { Component, ViewEncapsulation, viewChild, inject } from '@angular/core'; +import { + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { PROFILE_FORM } from '../../mocks/profile.mock'; +import { FormControlType } from '../../model/question'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +@Component({ + template: + '
', + standalone: false, +}) +class DummyComponent { + private readonly fb = inject(FormBuilder); + + readonly dynamicForm = + viewChild.required('dynamicForm'); + public testForm!: FormGroup; + public format = PROFILE_FORM; + constructor() { + this.testForm = this.fb.group({ + test: [''], + }); + } +} + +describe('DynamicFormComponent', () => { + let dummy: DummyComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + let component: DynamicFormComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DummyComponent], + imports: [ + DynamicFormComponent, + ReactiveFormsModule, + FormsModule, + NoopAnimationsModule, + ], + }) + .overrideComponent(DummyComponent, { + set: { encapsulation: ViewEncapsulation.None }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(DummyComponent); + + dummy = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + component = dummy.dynamicForm(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + PROFILE_FORM.forEach((item, index) => { + it(`should have form field with specific type"`, () => { + const fields = compiled.querySelectorAll('.form-field'); + + if (item.type === FormControlType.SELECT) { + const select = fields[index].querySelector('mat-select'); + expect(select).toBeTruthy(); + } else if (item.type === FormControlType.SELECT_MULTIPLE) { + const select = fields[index].querySelector('mat-checkbox'); + expect(select).toBeTruthy(); + } else if (item.type === FormControlType.TEXTAREA) { + const input = fields[index]?.querySelector('textarea'); + expect(input).toBeTruthy(); + } else { + const input = fields[index]?.querySelector('input'); + expect(input).toBeTruthy(); + } + }); + + it('should have label', () => { + const labels = compiled.querySelectorAll('.field-label'); + + const label = item.question; + expect(labels[index].textContent?.trim()).toEqual(label); + }); + + it('should have hint', () => { + const fields = compiled.querySelectorAll('.form-field'); + const hint = fields[index].querySelector('mat-hint'); + + if (item.description) { + expect(hint?.textContent?.trim()).toEqual(item.description); + } else { + expect(hint).toBeNull(); + } + }); + + if (item.type === FormControlType.SELECT) { + describe('select', () => { + it(`should have default value if provided`, () => { + const fields = compiled.querySelectorAll('.form-field'); + const select = fields[index].querySelector('mat-select'); + expect(select?.textContent?.trim()).toEqual(item.default || ''); + }); + + it('should have "required" error when field is not filled', () => { + const fields = compiled.querySelectorAll('.form-field'); + + component.getControl(index).setValue(''); + component.getControl(index).markAsTouched(); + + fixture.detectChanges(); + + const error = fields[index].querySelector('mat-error')?.innerHTML; + + expect(error).toContain('The field is required'); + }); + }); + } + + if (item.type === FormControlType.SELECT_MULTIPLE) { + describe('select multiple', () => { + it(`should mark form group as dirty while tab navigation`, () => { + const fields = compiled.querySelectorAll('.form-field'); + const checkbox = fields[index].querySelector( + '.field-select-checkbox:last-of-type mat-checkbox' + ); + checkbox?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' })); + fixture.detectChanges(); + + expect(component.getControl(index).dirty).toBeTrue(); + }); + }); + } + + if ( + item.type === FormControlType.TEXT || + item.type === FormControlType.TEXTAREA || + item.type === FormControlType.EMAIL_MULTIPLE + ) { + describe('text or text-long or email-multiple', () => { + if (item.validation?.required) { + it('should have "required" error when field is not filled', () => { + const fields = compiled.querySelectorAll('.form-field'); + const input = fields[index].querySelector( + '.mat-mdc-input-element' + ) as HTMLInputElement; + ['', ' '].forEach(value => { + input.value = value; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + const errors = fields[index].querySelectorAll('mat-error'); + let hasError = false; + errors.forEach(error => { + if (error.textContent === 'The field is required') { + hasError = true; + } + }); + + expect(hasError).toBeTrue(); + }); + }); + } + + it('should have "invalid_format" error when field does not satisfy validation rules', () => { + const fields = compiled.querySelectorAll('.form-field'); + const input: HTMLInputElement = fields[index].querySelector( + '.mat-mdc-input-element' + ) as HTMLInputElement; + input.value = 'as\\\\\\\\\\""""""""'; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + const result = + item.type === FormControlType.EMAIL_MULTIPLE + ? 'Please, check the email address. Valid e-mail can contain only latin letters, numbers, @ and . (dot).' + : 'Please, check. “ and \\ are not allowed.'; + const errors = fields[index].querySelectorAll('mat-error'); + let hasError = false; + errors.forEach(error => { + if (error.textContent === result) { + hasError = true; + } + }); + + expect(hasError).toBeTrue(); + }); + + if (item.validation?.max) { + it('should have "maxlength" error when field is exceeding max length', () => { + const fields = compiled.querySelectorAll('.form-field'); + const input: HTMLInputElement = fields[index].querySelector( + '.mat-mdc-input-element' + ) as HTMLInputElement; + input.value = + 'very long value very long value very long value very long value very long value very long value very long value very long value very long value very long value'; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + + const errors = fields[index].querySelectorAll('mat-error'); + let hasError = false; + errors.forEach(error => { + if ( + error.textContent === + `The field must be a maximum of ${item.validation?.max} characters.` + ) { + hasError = true; + } + }); + expect(hasError).toBeTrue(); + }); + } + }); + } + }); + + describe('adjustSubscriptWrapperHeights', () => { + it('should set height for hint wrapper', () => { + component.adjustSubscriptWrapperHeights(); + + const wrapper = compiled.querySelector( + '.mat-mdc-form-field-subscript-wrapper' + ); + expect(wrapper?.clientHeight).toEqual(20); + }); + }); +}); diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts new file mode 100644 index 000000000..b0abc5eb3 --- /dev/null +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts @@ -0,0 +1,213 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Component, + HostListener, + inject, + Input, + OnInit, + Renderer2, + viewChildren, + ViewEncapsulation, +} from '@angular/core'; +import { + FormControlType, + OptionType, + QuestionFormat, + Validation, +} from '../../model/question'; +import { + AbstractControl, + ControlContainer, + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + ValidatorFn, + Validators, +} from '@angular/forms'; +import { + MatError, + MatFormField, + MatOption, + MatSelectModule, +} from '@angular/material/select'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule } from '@angular/common'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { TextFieldModule } from '@angular/cdk/text-field'; +import { ProfileValidators } from '../../pages/risk-assessment/profile-form/profile.validators'; +import { DomSanitizer } from '@angular/platform-browser'; +@Component({ + selector: 'app-dynamic-form', + + imports: [ + MatFormField, + MatOption, + MatButtonModule, + CommonModule, + ReactiveFormsModule, + MatInputModule, + MatError, + MatFormFieldModule, + MatSelectModule, + MatCheckboxModule, + TextFieldModule, + ], + viewProviders: [ + { + provide: ControlContainer, + useFactory: () => inject(ControlContainer, { skipSelf: true }), + }, + ], + templateUrl: './dynamic-form.component.html', + styleUrl: './dynamic-form.component.scss', + encapsulation: ViewEncapsulation.None, +}) +export class DynamicFormComponent implements OnInit { + readonly formFields = viewChildren(MatFormField); + + private fb = inject(FormBuilder); + private profileValidators = inject(ProfileValidators); + private domSanitizer = inject(DomSanitizer); + private renderer = inject(Renderer2); + + public readonly FormControlType = FormControlType; + + @Input() format: QuestionFormat[] = []; + @Input() optionKey: string | undefined; + @HostListener('window:resize') + onResize() { + this.adjustSubscriptWrapperHeights(); + } + + parentContainer = inject(ControlContainer); + get formGroup() { + return this.parentContainer.control as FormGroup; + } + getControl(name: string | number) { + return this.formGroup.get(name.toString()) as AbstractControl; + } + + getFormGroup(name: string | number): FormGroup { + return this.formGroup?.controls[name] as FormGroup; + } + + public markSectionAsDirty( + optionIndex: number, + optionLength: number, + formControlName: string + ) { + if (optionIndex === optionLength - 1) { + this.getControl(formControlName).markAsDirty({ + onlySelf: true, + }); + } + } + + ngOnInit() { + this.createProfileForm(this.format); + } + + createProfileForm(questions: QuestionFormat[]) { + questions.forEach((question, index) => { + if (question.type === FormControlType.SELECT_MULTIPLE) { + this.formGroup.addControl( + index.toString(), + this.getMultiSelectGroup(question) + ); + } else { + const validators = this.getValidators( + question.type, + question.validation + ); + this.formGroup.addControl( + index.toString(), + new FormControl(question.default || '', validators) + ); + } + }); + } + + getValidators(type: FormControlType, validation?: Validation): ValidatorFn[] { + const validators: ValidatorFn[] = []; + if (validation) { + if (validation.required) { + validators.push(this.profileValidators.textRequired()); + } + if (validation.max) { + validators.push(Validators.maxLength(Number(validation.max))); + } + if (type === FormControlType.EMAIL_MULTIPLE) { + validators.push(this.profileValidators.emailStringFormat()); + } + if (type === FormControlType.TEXT || type === FormControlType.TEXTAREA) { + validators.push(this.profileValidators.textFormat()); + } + } + return validators; + } + + getMultiSelectGroup(question: QuestionFormat): FormGroup { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const group: any = {}; + question.options?.forEach((option, index) => { + group[index] = false; + }); + return this.fb.group(group, { + validators: question.validation?.required + ? [this.profileValidators.multiSelectRequired] + : [], + }); + } + + getOptionValue(option: OptionType) { + if (this.optionKey && typeof option === 'object') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (option as any)[this.optionKey]; + } + return option; + } + + getSanitizedOptionValue(option: OptionType) { + return this.domSanitizer.bypassSecurityTrustHtml( + this.getOptionValue(option) + ); + } + + adjustSubscriptWrapperHeights(): void { + this.formFields().forEach(formField => { + const matFormField = formField._elementRef.nativeElement; + if (matFormField) { + const subscriptWrapper = matFormField.querySelector( + '.mat-mdc-form-field-subscript-wrapper' + ) as HTMLElement; + const hint = matFormField.querySelector( + '.mat-mdc-form-field-hint' + ) as HTMLElement; + if (subscriptWrapper && hint) { + this.renderer.setStyle( + subscriptWrapper, + 'height', + `${hint.offsetHeight}px` + ); + } + } + }); + } +} diff --git a/modules/ui/src/app/components/empty-message/empty-message.component.html b/modules/ui/src/app/components/empty-message/empty-message.component.html new file mode 100644 index 000000000..ad15f66f0 --- /dev/null +++ b/modules/ui/src/app/components/empty-message/empty-message.component.html @@ -0,0 +1,28 @@ + +
+
+ empty message image +
+ + {{ header() }} + {{ message() }} + {{ messageNext() }} + + +
diff --git a/modules/ui/src/app/components/empty-message/empty-message.component.scss b/modules/ui/src/app/components/empty-message/empty-message.component.scss new file mode 100644 index 000000000..2729a3476 --- /dev/null +++ b/modules/ui/src/app/components/empty-message/empty-message.component.scss @@ -0,0 +1,73 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use 'variables'; +@use 'colors'; +@use 'mixins'; + +.empty-message.vertical { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + text-align: center; + .empty-message-main { + padding-top: 12px; + } +} + +.empty-message.horizontal { + display: grid; + grid-template-columns: auto 1fr; + align-items: end; + gap: 32px; + max-width: 886px; + .empty-message-img { + grid-row: 1 / 3; + justify-self: end; + } + .empty-message-text { + align-self: start; + padding-top: 12px; + &.one-line { + grid-row: 1 / 3; + align-self: center; + padding-top: 0; + } + } + .empty-message-main { + padding-top: 8px; + } +} + +.empty-message-header { + @include mixins.headline-small; +} + +.empty-message-main { + font-family: variables.$font-secondary; + font-weight: 400; + font-size: 16px; + line-height: 24px; + letter-spacing: 0px; + color: colors.$on-surface-variant; + display: block; +} + +.empty-message-text { + .empty-message-main.next-line { + padding-top: 0; + } +} diff --git a/modules/ui/src/app/components/empty-message/empty-message.component.spec.ts b/modules/ui/src/app/components/empty-message/empty-message.component.spec.ts new file mode 100644 index 000000000..7fecdbff0 --- /dev/null +++ b/modules/ui/src/app/components/empty-message/empty-message.component.spec.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EmptyMessageComponent } from './empty-message.component'; + +describe('EmptyMessageComponent', () => { + let compiled: HTMLElement; + let component: EmptyMessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EmptyMessageComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(EmptyMessageComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + fixture.componentRef.setInput('image', 'image.csv'); + fixture.componentRef.setInput('header', 'header text'); + fixture.componentRef.setInput('message', 'message text'); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have image', () => { + const image = compiled.querySelector('img') as HTMLImageElement; + + expect(image?.src).toContain('image.csv'); + }); + + it('should have header', () => { + const text = compiled.querySelector('.empty-message-header'); + + expect(text?.textContent?.trim()).toEqual('header text'); + }); + + it('should have message', () => { + const text = compiled.querySelector('.empty-message-main'); + + expect(text?.textContent?.trim()).toEqual('message text'); + }); +}); diff --git a/modules/ui/src/app/components/empty-message/empty-message.component.ts b/modules/ui/src/app/components/empty-message/empty-message.component.ts new file mode 100644 index 000000000..bf51345e2 --- /dev/null +++ b/modules/ui/src/app/components/empty-message/empty-message.component.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, input } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-empty-message', + imports: [CommonModule], + templateUrl: './empty-message.component.html', + styleUrl: './empty-message.component.scss', +}) +export class EmptyMessageComponent { + image = input(); + header = input(); + message = input(); + messageNext = input(); + isHorizontal = input(false); +} diff --git a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.html b/modules/ui/src/app/components/empty-page/empty-page.component.html similarity index 54% rename from modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.html rename to modules/ui/src/app/components/empty-page/empty-page.component.html index 0614dfc28..c0daf35f8 100644 --- a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.html +++ b/modules/ui/src/app/components/empty-page/empty-page.component.html @@ -13,26 +13,11 @@ See the License for the specific language governing permissions and limitations under the License. --> -{{ data.title }} - - - - - - + + + diff --git a/modules/ui/src/app/components/empty-page/empty-page.component.spec.ts b/modules/ui/src/app/components/empty-page/empty-page.component.spec.ts new file mode 100644 index 000000000..fb842f6c4 --- /dev/null +++ b/modules/ui/src/app/components/empty-page/empty-page.component.spec.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EmptyPageComponent } from './empty-page.component'; + +describe('EmptyPageComponent', () => { + let component: EmptyPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EmptyPageComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(EmptyPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment-routing.module.ts b/modules/ui/src/app/components/empty-page/empty-page.component.ts similarity index 59% rename from modules/ui/src/app/pages/risk-assessment/risk-assessment-routing.module.ts rename to modules/ui/src/app/components/empty-page/empty-page.component.ts index 927da40bc..f3ed1a15e 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment-routing.module.ts +++ b/modules/ui/src/app/components/empty-page/empty-page.component.ts @@ -13,14 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { RiskAssessmentComponent } from './risk-assessment.component'; +import { Component, input } from '@angular/core'; +import { EmptyMessageComponent } from '../empty-message/empty-message.component'; -const routes: Routes = [{ path: '', component: RiskAssessmentComponent }]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], +@Component({ + selector: 'app-empty-page', + imports: [EmptyMessageComponent], + templateUrl: './empty-page.component.html', }) -export class RiskAssessmentRoutingModule {} +export class EmptyPageComponent { + image = input(); + header = input(); + message = input(); + messageNext = input(); +} diff --git a/modules/ui/src/app/components/escapable-dialog/escapable-dialog.component.ts b/modules/ui/src/app/components/escapable-dialog/escapable-dialog.component.ts index d1a842dab..023dcca25 100644 --- a/modules/ui/src/app/components/escapable-dialog/escapable-dialog.component.ts +++ b/modules/ui/src/app/components/escapable-dialog/escapable-dialog.component.ts @@ -13,19 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { filter, take } from 'rxjs'; import { MatDialogRef } from '@angular/material/dialog'; @Component({ selector: 'app-escapable-dialog', - standalone: true, + imports: [CommonModule], template: '', }) export class EscapableDialogComponent { - constructor(public dialogRef: MatDialogRef) { + dialogRef = inject>(MatDialogRef); + + constructor() { + const dialogRef = this.dialogRef; + this.dialogRef .keydownEvents() .pipe( diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.html b/modules/ui/src/app/components/help-tip/help-tip.component.html new file mode 100644 index 000000000..3d1873f3f --- /dev/null +++ b/modules/ui/src/app/components/help-tip/help-tip.component.html @@ -0,0 +1,49 @@ + + diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.scss b/modules/ui/src/app/components/help-tip/help-tip.component.scss new file mode 100644 index 000000000..7c3e2953f --- /dev/null +++ b/modules/ui/src/app/components/help-tip/help-tip.component.scss @@ -0,0 +1,111 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@use 'm3-theme' as *; +@use 'colors'; +@use 'variables'; + +.tip { + position: absolute; + z-index: 100; + width: 256px; + box-sizing: border-box; + + &.left { + width: 276px; + } +} + +.tip-container { + position: relative; + border-radius: 28px; + background: colors.$primary; + color: colors.$white; + box-shadow: + 0px 4px 8px 3px rgba(0, 0, 0, 0.15), + 0px 1px 3px 0px rgba(0, 0, 0, 0.3); + + p { + margin: 0; + } +} + +.tip-container::before { + content: ''; + position: absolute; + border-radius: 4px; + height: 20px; + width: 20px; + background: colors.$primary; + box-sizing: border-box; + transform: rotate(45deg) translate(-50%); +} + +.top .tip-container::before { + top: 0; + left: 50%; +} + +.left .tip-container::before { + top: 50%; + left: 0; +} + +.heading { + display: flex; + justify-content: space-between; + align-items: flex-end; + padding: 4px 4px 0 24px; +} + +.title { + font-family: variables.$font-text; + font-size: 16px; + font-weight: 500; + line-height: 24px; +} + +.close-button { + margin-bottom: 4px; + + mat-icon { + color: colors.$white; + } +} + +.tip-content { + padding: 0 24px; + font-family: variables.$font-text; + font-size: 14px; + line-height: 20px; +} + +.tip-action-container { + display: flex; + justify-content: flex-end; + padding: 16px 12px 14px 24px; + + .tip-action-link { + display: inline-block; + padding: 10px 24px; + text-decoration: none; + cursor: pointer; + font-family: variables.$font-text; + font-size: 14px; + font-weight: 500; + line-height: 20px; + } +} diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.spec.ts b/modules/ui/src/app/components/help-tip/help-tip.component.spec.ts new file mode 100644 index 000000000..89160cedb --- /dev/null +++ b/modules/ui/src/app/components/help-tip/help-tip.component.spec.ts @@ -0,0 +1,134 @@ +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; + +import { HelpTipComponent } from './help-tip.component'; +import { HelpTips } from '../../model/tip-config'; +import SpyObj = jasmine.SpyObj; +import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { FocusManagerService } from '../../services/focus-manager.service'; + +describe('HelpTipComponent', () => { + let component: HelpTipComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + const mockLiveAnnouncer: SpyObj = jasmine.createSpyObj([ + 'announce', + 'clear', + ]); + + const mockFocusManagerService: SpyObj = + jasmine.createSpyObj('mockFocusManagerService', [ + 'focusFirstElementInContainer', + ]); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HelpTipComponent], + providers: [ + { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, + { provide: FocusManagerService, useValue: mockFocusManagerService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(HelpTipComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('data', HelpTips.step1); + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set focus to first focusable elem', fakeAsync(() => { + component.ngOnInit(); + tick(200); + + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalled(); + })); + + it('should have provided data', () => { + const tipTitle = compiled.querySelector('.tip-container .title'); + const tipContent = compiled.querySelector('.tip-container .tip-content'); + + expect(tipTitle?.innerHTML.trim()).toContain(HelpTips.step1.title); + expect(tipContent?.innerHTML.trim()).toContain(HelpTips.step1.content); + }); + + it('should have class provided from arrowPosition', () => { + const tipEl = compiled.querySelector('.tip'); + + expect(tipEl?.classList).toContain('top'); + }); + + describe('#updateTipPosition', () => { + beforeEach(() => { + const mockTarget = document.createElement('div'); + spyOn(mockTarget, 'getBoundingClientRect').and.returnValue({ + top: 100, + left: 100, + height: 100, + width: 100, + bottom: 100, + right: 100, + } as DOMRect); + fixture.componentRef.setInput('target', mockTarget); + fixture.detectChanges(); + }); + + it('should update tip position when data.position as "bottom"', () => { + component.ngOnInit(); + + expect(component.tipPosition.left).toBe(22); + expect(component.tipPosition.top).toBe(114); + }); + + it('should update tip position when data.position as "right"', fakeAsync(() => { + fixture.componentRef.setInput('data', HelpTips.step2); + tick(); + + component.ngOnInit(); + + expect(component.tipPosition.left).toBe(100); + expect(component.tipPosition.top).toBe(68); + })); + + it('should update tip position when data.position as "left"', fakeAsync(() => { + const mockData = { ...HelpTips.step2, position: 'left' }; + fixture.componentRef.setInput('data', mockData); + tick(); + + component.ngOnInit(); + + expect(component.tipPosition.left).toBe(-170); + expect(component.tipPosition.top).toBe(150); + })); + + it('should update tip position when data.position as "top"', fakeAsync(() => { + const mockData = { ...HelpTips.step2, position: 'top' }; + fixture.componentRef.setInput('data', mockData); + tick(); + + component.ngOnInit(); + + expect(component.tipPosition.left).toBe(22); + expect(component.tipPosition.top).toBe(86); + })); + + it('should call updateTipPosition on window resize', () => { + spyOn(component, 'updateTipPosition'); + + window.dispatchEvent(new Event('resize')); + + expect(component.updateTipPosition).toHaveBeenCalled(); + }); + }); +}); diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.ts b/modules/ui/src/app/components/help-tip/help-tip.component.ts new file mode 100644 index 000000000..300602165 --- /dev/null +++ b/modules/ui/src/app/components/help-tip/help-tip.component.ts @@ -0,0 +1,122 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + ChangeDetectionStrategy, + Component, + HostListener, + inject, + input, + OnInit, + output, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { TipConfig } from '../../model/tip-config'; +import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { FocusManagerService } from '../../services/focus-manager.service'; +import { timer } from 'rxjs/internal/observable/timer'; + +@Component({ + selector: 'app-help-tip', + imports: [CommonModule, MatIconModule, MatButtonModule], + templateUrl: './help-tip.component.html', + styleUrl: './help-tip.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HelpTipComponent implements OnInit { + data = input(); + target = input(); + action = input(); + onAction = output(); + onCLoseTip = output(); + tipPosition = { top: 0, left: 0 }; + private readonly liveAnnouncer = inject(LiveAnnouncer); + private readonly focusManagerService = inject(FocusManagerService); + + @HostListener('window:resize') + onResize() { + this.updateTipPosition(this.target()); + } + + ngOnInit() { + this.updateTipPosition(this.target()); + this.liveAnnouncer.announce( + `${this.data()?.title} ${this.data()?.content}` + ); + this.setFocus(); + } + + private setFocus(): void { + const helpTipEl = window.document.querySelector('.tip'); + timer(200).subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(helpTipEl); + }); + } + + updateTipPosition(target: HTMLElement | undefined | null) { + if (!target) { + return; + } + + const targetRect = target.getBoundingClientRect(); + + const tipWidth = 256; + const arrowOffset = 14; + const topOffset = 82; + + let top = 0; + let left = 0; + + switch (this.data()?.position) { + case 'left': + top = targetRect.top + window.scrollY + targetRect.height / 2; + left = targetRect.left + window.scrollX - tipWidth - arrowOffset; + break; + case 'right': + top = + targetRect.top + window.scrollY - topOffset + targetRect.height / 2; + left = targetRect.right + window.scrollX; + break; + case 'top': + top = targetRect.top + window.scrollY - arrowOffset; // Position above the button + left = + targetRect.left + + window.scrollX + + targetRect.width / 2 - + tipWidth / 2; // Center horizontally above button + break; + case 'bottom': + top = targetRect.bottom + window.scrollY + arrowOffset; // Position below the button + left = + targetRect.left + + window.scrollX + + targetRect.width / 2 - + tipWidth / 2; // Center horizontally below button + break; + default: + throw new Error('Unsupported tip position!'); + } + + this.tipPosition = { top, left }; + } + + onActionClick(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + this.onAction.emit(); + } +} diff --git a/modules/ui/src/app/components/list-item/list-item.component.html b/modules/ui/src/app/components/list-item/list-item.component.html new file mode 100644 index 000000000..2e01cce09 --- /dev/null +++ b/modules/ui/src/app/components/list-item/list-item.component.html @@ -0,0 +1,41 @@ + +
+ + +
+ + + + diff --git a/modules/ui/src/app/components/list-item/list-item.component.scss b/modules/ui/src/app/components/list-item/list-item.component.scss new file mode 100644 index 000000000..71f40550d --- /dev/null +++ b/modules/ui/src/app/components/list-item/list-item.component.scss @@ -0,0 +1,48 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use 'variables'; +@use 'colors'; +@use '@angular/material' as mat; + +::ng-deep { + .list-item-menu { + width: 220px; + } +} + +.list-item { + border-radius: variables.$corner-large; + background-color: colors.$surface-container; + height: 92px; + padding: 0 24px 0 32px; + display: grid; + grid-template-columns: auto 40px; + align-items: center; + &.disabled { + cursor: not-allowed; + } +} + +.example-menu { + left: 12px; +} + +.list-item-menu-item { + padding: 16px 24px; + &.cdk-mouse-focused::before { + content: none; + } +} diff --git a/modules/ui/src/app/components/list-item/list-item.component.spec.ts b/modules/ui/src/app/components/list-item/list-item.component.spec.ts new file mode 100644 index 000000000..f671c57c7 --- /dev/null +++ b/modules/ui/src/app/components/list-item/list-item.component.spec.ts @@ -0,0 +1,107 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatMenuModule } from '@angular/material/menu'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ListItemComponent } from './list-item.component'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { + MatMenuHarness, + MatMenuItemHarness, +} from '@angular/material/menu/testing'; + +interface Entity { + id: number; + name: string; +} + +describe('ListItemComponent', () => { + let component: ListItemComponent; + let fixture: ComponentFixture>; + let compiled: HTMLElement; + let loader: HarnessLoader; + const testActions = [ + { action: 'Edit', icon: 'edit_icon' }, + { action: 'Delete', icon: 'delete_icon' }, + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + ListItemComponent, + MatButtonModule, + MatIconModule, + MatMenuModule, + NoopAnimationsModule, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ListItemComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('actions', testActions); + fixture.componentRef.setInput('entity', { id: 1, name: 'test' } as Entity); + compiled = fixture.nativeElement as HTMLElement; + loader = TestbedHarnessEnvironment.loader(fixture); + fixture.detectChanges(); + }); + + describe('menu', () => { + let menu; + let items: MatMenuItemHarness[]; + + beforeEach(async () => { + menu = await loader.getHarness(MatMenuHarness); + await menu.open(); + items = await menu.getItems(); + }); + + it('should render actions in the menu', async () => { + expect(items.length).toBe(2); + + const text0 = await items[0].getText(); + const text1 = await items[1].getText(); + + expect(text0).toContain('Edit'); + expect(text1).toContain('Delete'); + }); + + it('should emit the correct action when a menu item is clicked', async () => { + const menuItemClickedSpy = spyOn(component.menuItemClicked, 'emit'); + + await items[0].click(); + + expect(menuItemClickedSpy).toHaveBeenCalledWith('Edit'); + }); + + it('should display the correct icons for actions', async () => { + const text0 = await items[0].getText(); + const text1 = await items[1].getText(); + + expect(text0).toContain('edit'); + expect(text1).toContain('delete'); + }); + }); + + it('should render menu button', () => { + const button = compiled.querySelector('button[mat-icon-button]'); + + expect(button).toBeTruthy(); + expect(button?.textContent).toContain('more_vert'); + }); +}); diff --git a/modules/ui/src/app/components/list-item/list-item.component.ts b/modules/ui/src/app/components/list-item/list-item.component.ts new file mode 100644 index 000000000..2728ee503 --- /dev/null +++ b/modules/ui/src/app/components/list-item/list-item.component.ts @@ -0,0 +1,117 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Component, + HostListener, + inject, + input, + output, + OnInit, + ChangeDetectionStrategy, + NgZone, +} from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { CommonModule } from '@angular/common'; +import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; +import { EntityAction } from '../../model/entity-action'; +import { MatTooltip } from '@angular/material/tooltip'; + +@Component({ + selector: 'app-list-item', + imports: [ + MatButtonModule, + MatIconModule, + MatMenuTrigger, + MatMenu, + MatMenuItem, + CommonModule, + ], + providers: [MatTooltip], + templateUrl: './list-item.component.html', + styleUrl: './list-item.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ListItemComponent implements OnInit { + private container?: HTMLElement; + private zone = inject(NgZone); + entity = input.required(); + actions = input([]); + menuItemClicked = output(); + tooltip = inject(MatTooltip); + isDisabled = input<(arg: T) => boolean>(); + tooltipMessage = input<(arg: T) => string>(); + + get disabled() { + const isDisabledFn = this.isDisabled(); + if (isDisabledFn) { + return isDisabledFn(this.entity()); + } + return false; + } + + @HostListener('mouseenter', ['$event']) + onEvent(event: MouseEvent): void { + this.zone.run(() => { + this.updateMessage(); + if (!this.tooltip.message) return; + this.tooltip.show(0, { x: event.clientX, y: event.clientY }); + this.container = document.querySelector( + '.mat-mdc-tooltip-panel:has(.list-item-tooltip)' + ) as HTMLElement; + }); + } + + @HostListener('mousemove', ['$event']) + onMoveEvent(event: MouseEvent): void { + this.zone.run(() => { + if (!this.tooltip.message) return; + if (!this.container) { + this.container = document.querySelector( + '.mat-mdc-tooltip-panel:has(.list-item-tooltip)' + ) as HTMLElement; + } + + this.container.style.top = event.clientY + 'px'; + this.container.style.left = event.clientX + 'px'; + }); + } + + @HostListener('mouseleave') + outEvent(): void { + this.tooltip.hide(); + } + + ngOnInit() { + this.updateMessage(); + this.tooltip.positionAtOrigin = true; + this.tooltip.tooltipClass = 'list-item-tooltip'; + } + + trackByAction(index: number, item: EntityAction) { + return item.action; + } + + private updateMessage() { + const tooltipMessageFn = this.tooltipMessage(); + if (tooltipMessageFn) { + const tooltipMessage = tooltipMessageFn(this.entity()); + if (tooltipMessage) { + this.tooltip.message = tooltipMessage; + } + } + } +} diff --git a/modules/ui/src/app/components/list-layout/list-layout.component.html b/modules/ui/src/app/components/list-layout/list-layout.component.html new file mode 100644 index 000000000..55ffb7c1b --- /dev/null +++ b/modules/ui/src/app/components/list-layout/list-layout.component.html @@ -0,0 +1,87 @@ + + + + + +

{{ title() }}

+ + + + +
+ + search +
+
+
+
+
+ + {{ + title() === LayoutType.Device ? 'New device' : 'New risk profile' + }} +
+ + + + + +
+
+
+ +
+ +
+
+
+
+ +

{{ title() }}

+
+ +
+
diff --git a/modules/ui/src/app/components/list-layout/list-layout.component.scss b/modules/ui/src/app/components/list-layout/list-layout.component.scss new file mode 100644 index 000000000..c00a11764 --- /dev/null +++ b/modules/ui/src/app/components/list-layout/list-layout.component.scss @@ -0,0 +1,153 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use 'mixins'; +@use 'colors'; +@use 'variables'; + +:host { + position: relative; + height: 100%; + display: block; +} + +:host ::ng-deep .mat-drawer-inner-container { + overflow: hidden; + display: grid; + grid-template-rows: max-content 1fr; +} + +.entity-list app-list-item:has(.selected) { + ::ng-deep .list-item { + background-color: colors.$primary-container; + } +} + +.content { + height: 100%; +} + +.content-empty { + @include mixins.content-empty; +} + +.title { + color: colors.$on-surface; + font-family: variables.$font-primary; + font-size: 32px; + font-style: normal; + font-weight: 400; + line-height: 40px; + &-empty { + padding: 24px 0 8px 32px; + margin: 0; + } +} + +.add-entity-button { + font-family: variables.$font-text; + font-size: 16px; + border-radius: 16px; + width: fit-content; + height: 56px; + color: colors.$on-secondary-container; + background-color: colors.$secondary-container; + &:disabled { + opacity: 0.5; + pointer-events: none; + } +} + +.layout-container { + height: 100%; +} + +.layout-container-left-panel { + background-color: colors.$surface-container-low; + width: 435px; + padding-right: 16px; + overflow: hidden; +} + +.layout-container-left-panel-toolbar { + border-radius: variables.$corner-large; + background-color: colors.$surface; + padding: 12px 0 8px 16px; + ::ng-deep mat-toolbar-row:not(:first-child) { + margin-top: 32px; + } +} + +.search-field { + display: flex; + padding: 4px 4px 4px 20px; + align-items: center; + gap: 4px; + flex: 1 0 0; + align-self: stretch; + border-radius: variables.$corner-extra-large; + background-color: colors.$surface-container-high; + height: 40px; + width: 100%; + input { + width: calc(100% - #{variables.$icon-size * 2}); + height: 100%; + border: 0; + background: inherit; + font-size: 16px; + font-family: variables.$font-text; + color: colors.$on-surface-variant; + } +} + +::ng-deep .using-mouse .search-field input:focus { + outline: none; +} + +.entity-list-container { + overflow-y: auto; +} + +.entity-list { + display: grid; + grid-template-columns: 1fr; + gap: 8px; + padding: 8px 0; +} + +.add-entity-button { + font-family: variables.$font-text; + font-size: 16px; + border-radius: 16px; + width: fit-content; + height: 56px; + color: colors.$on-secondary-container; + background-color: colors.$secondary-container; +} + +.fake-list-item { + border-radius: 16px; + background-color: colors.$primary-container; + height: 92px; + padding: 0 24px 0 32px; + display: flex; + gap: 24px; + align-items: center; + color: colors.$on-surface; + font-weight: 500; + font-family: variables.$font-text; + font-size: 16px; + letter-spacing: 0; +} diff --git a/modules/ui/src/app/components/list-layout/list-layout.component.spec.ts b/modules/ui/src/app/components/list-layout/list-layout.component.spec.ts new file mode 100644 index 000000000..02089934e --- /dev/null +++ b/modules/ui/src/app/components/list-layout/list-layout.component.spec.ts @@ -0,0 +1,150 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ListLayoutComponent } from './list-layout.component'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { ListItemComponent } from '../list-item/list-item.component'; +import { Component } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +interface Entity { + id: number; + name: string; +} +@Component({ + selector: 'app-host-component', + imports: [ListLayoutComponent, ListItemComponent], + template: ` + + + +
{{ entity.name }}
+
+ + +
Empty
+
+ `, +}) +class HostComponent { + title = 'Test Title'; + addEntityText = 'Add Entity'; + entities: Entity[] = []; + actions = [{ label: 'Edit', value: 'edit' }]; + onAddEntity = jasmine.createSpy('onAddEntity'); + onMenuItemClicked = jasmine.createSpy('onMenuItemClicked'); +} + +describe('ListLayoutComponent', () => { + let component: HostComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + HostComponent, + MatSidenavModule, + MatToolbarModule, + MatIconModule, + MatButtonModule, + NoopAnimationsModule, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(HostComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + describe('with no entities', () => { + beforeEach(() => { + component.entities = []; + fixture.detectChanges(); + }); + + it('should display the title', () => { + const titleElement = compiled.querySelector('.title'); + + expect(titleElement?.textContent).toBe('Test Title'); + }); + + it('should has empty content', () => { + const emptyContent = compiled.querySelector('.content-empty'); + + expect(emptyContent).toBeTruthy(); + }); + }); + + describe('with entities', () => { + beforeEach(() => { + component.entities = [ + { id: 1, name: 'Entity 1' }, + { id: 2, name: 'Entity 2' }, + ]; + fixture.detectChanges(); + }); + + it('should display the title', () => { + const titleElement = compiled.querySelector('.title'); + + expect(titleElement?.textContent).toBe('Test Title'); + }); + + it('should display add entity button', () => { + const buttonElement = compiled.querySelector('.add-entity-button'); + + expect(buttonElement?.textContent).toContain('Add Entity'); + }); + + it('should display search field', () => { + const searchElement = compiled.querySelector('.search-field'); + + expect(searchElement).toBeTruthy(); + }); + + it('should emit addEntity event when add entity button is clicked', () => { + const buttonElement = compiled.querySelector( + '.add-entity-button' + ) as HTMLButtonElement; + + buttonElement.click(); + expect(component.onAddEntity).toHaveBeenCalled(); + }); + + it('should have entity list', () => { + const listItemComponent = compiled.querySelectorAll('app-list-item'); + + expect(listItemComponent.length).toEqual(2); + }); + }); +}); diff --git a/modules/ui/src/app/components/list-layout/list-layout.component.ts b/modules/ui/src/app/components/list-layout/list-layout.component.ts new file mode 100644 index 000000000..a89412897 --- /dev/null +++ b/modules/ui/src/app/components/list-layout/list-layout.component.ts @@ -0,0 +1,141 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + input, + output, + signal, + TemplateRef, +} from '@angular/core'; +import { CommonModule, DatePipe } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatIconModule } from '@angular/material/icon'; +import { ListItemComponent } from '../list-item/list-item.component'; +import { EntityAction, EntityActionResult } from '../../model/entity-action'; +import { Device } from '../../model/device'; +import { LayoutType } from '../../model/layout-type'; +import { Profile } from '../../model/profile'; + +@Component({ + selector: 'app-list-layout', + imports: [ + CommonModule, + MatButtonModule, + MatSidenavModule, + MatToolbarModule, + MatIconModule, + ListItemComponent, + ], + providers: [DatePipe], + templateUrl: './list-layout.component.html', + styleUrl: './list-layout.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ListLayoutComponent { + private datePipe = inject(DatePipe); + readonly LayoutType = LayoutType; + title = input(''); + addEntityText = input(''); + entityDisabled = input<(arg: T) => boolean>(); + entityTooltip = input<(arg: T) => string>(); + isOpenEntityForm = input(false); + initialEntity = input(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + emptyContent = input>(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + content = input>(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + itemTemplate = input>(); + entities = input([]); + actions = input([]); + actionsFn = input<(arg: T) => EntityAction[]>(); + searchText = signal(''); + filtered = computed(() => { + return this.entities().filter(this.filter(this.searchText())); + }); + addEntity = output(); + menuItemClicked = output>(); + + getActions = (entity: T) => { + if (this.actionsFn()) { + // @ts-expect-error actionsFn is defined + return this.actionsFn()(entity); + } + return this.actions(); + }; + + updateQuery(e: Event) { + const input = e.target as HTMLInputElement; + const value = input.value; + if (value.trim() === '') { + input.value = ''; + } else { + const inputValue = value.trim(); + const searchValue = inputValue.length > 2 ? inputValue : ''; + this.searchText.set(searchValue); + } + } + + filter(searchText: string) { + return (item: T) => { + const filterItem = this.getObjectForFilter(item); + return Object.values(filterItem).some(value => { + return typeof value === 'string' + ? value.toLowerCase().includes(searchText.toLowerCase()) + : false; + }); + }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getObjectForFilter(item: any) { + if (this.title() === LayoutType.Device) { + const device = item as Device; + return { + model: device.model, + manufacturer: device.manufacturer, + }; + } else if (this.title() === LayoutType.Profile) { + const profile = item as Profile; + return { + name: profile.name, + risk: profile.risk, + created: this.getFormattedDateString(profile.created), + }; + } else { + return item; + } + } + + getFormattedDateString(createdDate: string | undefined) { + return createdDate + ? this.datePipe.transform(createdDate, 'dd MMM yyyy') + : ''; + } + + onMenuItemClick(action: string, entity: T, index: number) { + this.menuItemClicked.emit({ + action, + entity, + index, + }); + } +} diff --git a/modules/ui/src/app/components/no-entity-selected/no-entity-selected.component.html b/modules/ui/src/app/components/no-entity-selected/no-entity-selected.component.html new file mode 100644 index 000000000..03556f807 --- /dev/null +++ b/modules/ui/src/app/components/no-entity-selected/no-entity-selected.component.html @@ -0,0 +1,21 @@ + + + diff --git a/modules/ui/src/app/components/no-entity-selected/no-entity-selected.component.scss b/modules/ui/src/app/components/no-entity-selected/no-entity-selected.component.scss new file mode 100644 index 000000000..4a7bf4668 --- /dev/null +++ b/modules/ui/src/app/components/no-entity-selected/no-entity-selected.component.scss @@ -0,0 +1,46 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use 'colors'; +@use 'variables'; + +:host { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-image: url(/assets/icons/cornerstone.svg); + position: relative; +} + +.dog-image { + position: absolute; + bottom: 0; + right: 26px; + width: 20%; + height: auto; + display: block; +} + +::ng-deep app-empty-message { + .empty-message { + gap: 16px !important; + } + + .empty-message-header { + color: colors.$on-surface-variant !important; + font-family: variables.$font-secondary !important; + } +} diff --git a/modules/ui/src/app/components/no-entity-selected/no-entity-selected.component.ts b/modules/ui/src/app/components/no-entity-selected/no-entity-selected.component.ts new file mode 100644 index 000000000..3924c2aa3 --- /dev/null +++ b/modules/ui/src/app/components/no-entity-selected/no-entity-selected.component.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, input } from '@angular/core'; +import { EmptyMessageComponent } from '../empty-message/empty-message.component'; +@Component({ + selector: 'app-no-entity-selected', + imports: [EmptyMessageComponent], + templateUrl: './no-entity-selected.component.html', + styleUrl: './no-entity-selected.component.scss', +}) +export class NoEntitySelectedComponent { + image = input(); + header = input(); + message = input(); +} diff --git a/modules/ui/src/app/components/program-type-icon/program-type-con.component.spec.ts b/modules/ui/src/app/components/program-type-icon/program-type-con.component.spec.ts new file mode 100644 index 000000000..7a984eff0 --- /dev/null +++ b/modules/ui/src/app/components/program-type-icon/program-type-con.component.spec.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProgramTypeIconComponent } from './program-type-icon.component'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; + +describe('ProgramTypeIconComponent', () => { + let component: ProgramTypeIconComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ProgramTypeIconComponent, MatIconTestingModule], + }).compileComponents(); + fixture = TestBed.createComponent(ProgramTypeIconComponent); + component = fixture.componentInstance; + component.type = 'pilot'; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); + + it('should have svgIcon provided from type', () => { + const iconEl = compiled.querySelector('.icon'); + + expect(iconEl?.getAttribute('ng-reflect-svg-icon')).toEqual('pilot'); + }); +}); diff --git a/modules/ui/src/app/components/program-type-icon/program-type-icon.component.ts b/modules/ui/src/app/components/program-type-icon/program-type-icon.component.ts new file mode 100644 index 000000000..4066d98e0 --- /dev/null +++ b/modules/ui/src/app/components/program-type-icon/program-type-icon.component.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Input } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; + +@Component({ + selector: 'app-program-type-icon', + + imports: [MatIcon], + template: ` `, + styles: ` + :host { + display: inline-flex; + align-items: center; + } + .icon { + display: flex; + line-height: 16px; + } + `, +}) +export class ProgramTypeIconComponent { + @Input() type = ''; +} diff --git a/modules/ui/src/app/components/report-action/report-action.component.spec.ts b/modules/ui/src/app/components/report-action/report-action.component.spec.ts index b4d69b149..7408de89d 100644 --- a/modules/ui/src/app/components/report-action/report-action.component.spec.ts +++ b/modules/ui/src/app/components/report-action/report-action.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReportActionComponent } from './report-action.component'; import { MOCK_PROGRESS_DATA_COMPLIANT } from '../../mocks/testrun.mock'; +import { TestrunStatus } from '../../model/testrun-status'; describe('ReportActionComponent', () => { let component: ReportActionComponent; @@ -21,12 +22,25 @@ describe('ReportActionComponent', () => { expect(component).toBeTruthy(); }); - it('#getTestRunId should return data for title of link', () => { - const expectedResult = 'Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20'; + describe('#getTestRunId', () => { + it('should return data for title of link', () => { + const expectedResult = 'Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20'; - const result = component.getTestRunId(MOCK_PROGRESS_DATA_COMPLIANT); + const result = component.getTestRunId(MOCK_PROGRESS_DATA_COMPLIANT); - expect(result).toEqual(expectedResult); + expect(result).toEqual(expectedResult); + }); + + it('should return title as empty string if no device data', () => { + const MOCK_DATA_WITHOUT_DEVICE = { + ...MOCK_PROGRESS_DATA_COMPLIANT, + device: undefined as unknown, + } as TestrunStatus; + + const result = component.getTestRunId(MOCK_DATA_WITHOUT_DEVICE); + + expect(result).toEqual(''); + }); }); it('#getFormattedDateString should return date as string in the format "d MMM y H:mm"', () => { diff --git a/modules/ui/src/app/components/report-action/report-action.component.ts b/modules/ui/src/app/components/report-action/report-action.component.ts index 62dc39d58..a7a6a0965 100644 --- a/modules/ui/src/app/components/report-action/report-action.component.ts +++ b/modules/ui/src/app/components/report-action/report-action.component.ts @@ -1,20 +1,29 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + inject, +} from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; import { TestrunStatus } from '../../model/testrun-status'; @Component({ selector: 'app-report-action', - standalone: true, + imports: [CommonModule], template: '', providers: [DatePipe], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ReportActionComponent { + private datePipe = inject(DatePipe); + @Input() data!: TestrunStatus; - constructor(private datePipe: DatePipe) {} getTestRunId(data: TestrunStatus) { + if (!data.device) { + return ''; + } return `${data.device.manufacturer} ${data.device.model} ${ data.device.firmware } ${this.getFormattedDateString(data.started)}`; diff --git a/modules/ui/src/app/components/shutdown-app/shutdown-app.component.spec.ts b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.spec.ts index 522234f5e..f2bba357b 100644 --- a/modules/ui/src/app/components/shutdown-app/shutdown-app.component.spec.ts +++ b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.spec.ts @@ -25,9 +25,9 @@ import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { TestRunService } from '../../services/test-run.service'; import SpyObj = jasmine.SpyObj; import { of } from 'rxjs'; -import { ShutdownAppModalComponent } from '../shutdown-app-modal/shutdown-app-modal.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { WINDOW } from '../../providers/window.provider'; +import { SimpleDialogComponent } from '../simple-dialog/simple-dialog.component'; describe('ShutdownAppComponent', () => { let component: ShutdownAppComponent; @@ -71,7 +71,7 @@ describe('ShutdownAppComponent', () => { mockService.shutdownTestrun.and.returnValue(of(false)); spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), - } as MatDialogRef); + } as MatDialogRef); tick(); component.openShutdownModal(); @@ -83,7 +83,7 @@ describe('ShutdownAppComponent', () => { mockService.shutdownTestrun.and.returnValue(of(true)); spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), - } as MatDialogRef); + } as MatDialogRef); tick(); component.openShutdownModal(); diff --git a/modules/ui/src/app/components/shutdown-app/shutdown-app.component.ts b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.ts index 3e59eb768..131576b1f 100644 --- a/modules/ui/src/app/components/shutdown-app/shutdown-app.component.ts +++ b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.ts @@ -16,49 +16,50 @@ import { ChangeDetectionStrategy, Component, - Inject, Input, OnDestroy, + inject, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; import { MatDialog } from '@angular/material/dialog'; -import { ShutdownAppModalComponent } from '../shutdown-app-modal/shutdown-app-modal.component'; import { Subject, takeUntil } from 'rxjs'; import { TestRunService } from '../../services/test-run.service'; import { WINDOW } from '../../providers/window.provider'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { SimpleDialogComponent } from '../simple-dialog/simple-dialog.component'; @Component({ selector: 'app-shutdown-app', - standalone: true, + imports: [CommonModule, MatButtonModule, MatIcon, MatTooltipModule], templateUrl: './shutdown-app.component.html', - styleUrl: './shutdown-app.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ShutdownAppComponent implements OnDestroy { + dialog = inject(MatDialog); + private testRunService = inject(TestRunService); + private window = inject(WINDOW); + @Input() disable: boolean = false; private destroy$: Subject = new Subject(); - constructor( - public dialog: MatDialog, - private testRunService: TestRunService, - @Inject(WINDOW) private window: Window - ) {} openShutdownModal() { - const dialogRef = this.dialog.open(ShutdownAppModalComponent, { + const dialogRef = this.dialog.open(SimpleDialogComponent, { ariaLabel: 'Shutdown Testrun', data: { - title: 'Shutdown Testrun', - content: 'Do you want to stop Testrun?', + icon: 'power_settings_new', + title: 'Shutdown Testrun?', + content: + 'Testrun will shutdown and all testing processes will be stopped.', + confirmName: 'Stop Server & Quit', }, autoFocus: true, hasBackdrop: true, disableClose: true, - panelClass: 'shutdown-app-dialog', + panelClass: ['simple-dialog', 'shutdown-app-dialog'], }); dialogRef diff --git a/modules/ui/src/app/components/side-button-menu/side-button-menu.component.html b/modules/ui/src/app/components/side-button-menu/side-button-menu.component.html new file mode 100644 index 000000000..678434e95 --- /dev/null +++ b/modules/ui/src/app/components/side-button-menu/side-button-menu.component.html @@ -0,0 +1,56 @@ +
+ + +
+ + +
+ + + +
+ +
diff --git a/modules/ui/src/app/components/side-button-menu/side-button-menu.component.scss b/modules/ui/src/app/components/side-button-menu/side-button-menu.component.scss new file mode 100644 index 000000000..9fafcf71a --- /dev/null +++ b/modules/ui/src/app/components/side-button-menu/side-button-menu.component.scss @@ -0,0 +1,77 @@ +@use 'colors'; +@use 'variables'; +@use '@angular/material' as mat; + +:host { + display: flex; + justify-content: center; + width: 100%; +} + +.side-add-button-container { + position: relative; +} + +.side-add-menu-trigger { + position: absolute; + top: 0; + right: -20px; +} + +.side-add-menu-triangle { + position: absolute; + top: 18px; + left: -12px; +} + +::ng-deep .side-add-menu { + overflow: visible !important; + width: 278px; + border-radius: 4px; + padding: 0 8px; +} + +.side-add-button { + --mdc-fab-container-color: #{colors.$primary-container}; + --mat-icon-color: #{colors.$primary}; +} + +.side-add-menu-button { + gap: 12px; + border-radius: 4px; + width: 100%; + height: 56px; + display: grid; + background: inherit; + grid-template-columns: min-content auto; +} + +::ng-deep .using-mouse .side-add-menu-button { + &.cdk-mouse-focused, + &.cdk-program-focused { + background: inherit !important; + } + &.cdk-mouse-focused::before, + &.cdk-program-focused::before { + content: none; + } +} + +.side-add-menu-button-description { + color: colors.$on-surface-variant; + font-family: variables.$font-text; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.1px; +} + +.side-add-menu-button-label { + color: colors.$on-surface; + font-family: variables.$font-text; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; +} diff --git a/modules/ui/src/app/components/side-button-menu/side-button-menu.component.spec.ts b/modules/ui/src/app/components/side-button-menu/side-button-menu.component.spec.ts new file mode 100644 index 000000000..289b12daa --- /dev/null +++ b/modules/ui/src/app/components/side-button-menu/side-button-menu.component.spec.ts @@ -0,0 +1,126 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { By } from '@angular/platform-browser'; +import { of } from 'rxjs'; +import { SideButtonMenuComponent } from './side-button-menu.component'; +import { + MatMenuHarness, + MatMenuItemHarness, +} from '@angular/material/menu/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; + +describe('SideButtonMenuComponent', () => { + let component: SideButtonMenuComponent; + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + SideButtonMenuComponent, + MatMenuModule, + MatButtonModule, + MatIconModule, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SideButtonMenuComponent); + component = fixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(fixture); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render menu button', () => { + const button = fixture.debugElement.query(By.css('.side-add-button')); + expect(button).toBeTruthy(); + }); + + describe('menu', () => { + let menu; + let items: MatMenuItemHarness[]; + const onClickSpy = jasmine.createSpy('onClick'); + + beforeEach(async () => { + fixture.componentRef.setInput('menuItems', [ + { + icon: 'home', + label: 'Home', + onClick: () => {}, + disabled$: of(true), + }, + { + icon: 'settings', + label: 'Settings', + description: 'Settings description', + onClick: onClickSpy, + disabled$: of(false), + }, + ]); + fixture.detectChanges(); + + menu = await loader.getHarness(MatMenuHarness); + await menu.open(); + items = await menu.getItems(); + }); + + it('should render menu items', async () => { + expect(items.length).toBe(2); + + const text0 = await items[0].getText(); + const text1 = await items[1].getText(); + + expect(text0).toContain('Home'); + expect(text1).toContain('Settings'); + expect(text1).toContain('Settings description'); + }); + + it('should emit the correct action when a menu item is clicked', async () => { + await items[1].click(); + + expect(onClickSpy).toHaveBeenCalled(); + }); + + ['Escape', 'Tab'].forEach((key: string) => { + it(`should focus side button on ${key} press`, async () => { + const button = document.querySelector( + '.side-add-button' + ) as HTMLButtonElement; + const buttonFocusSpy = spyOn(button, 'focus'); + const firstItemElement = await items[0].host(); + await firstItemElement.dispatchEvent('keydown', { key: key }); + + expect(buttonFocusSpy).toHaveBeenCalled(); + }); + + it(`should close menu on ${key} press`, async () => { + const closeMenuSpy = spyOn(component.menuTrigger(), 'closeMenu'); + const firstItemElement = await items[0].host(); + await firstItemElement.dispatchEvent('keydown', { key: key }); + + expect(closeMenuSpy).toHaveBeenCalled(); + }); + }); + + it('should display the correct icons for actions', async () => { + const text0 = await items[0].getText(); + const text1 = await items[1].getText(); + + expect(text0).toContain('home'); + expect(text1).toContain('settings'); + }); + + it('should disable menu item when observable emits true', async () => { + const disabled = await items[0].isDisabled(); + expect(disabled).toBeTrue(); + }); + }); +}); diff --git a/modules/ui/src/app/components/side-button-menu/side-button-menu.component.ts b/modules/ui/src/app/components/side-button-menu/side-button-menu.component.ts new file mode 100644 index 000000000..f355d6706 --- /dev/null +++ b/modules/ui/src/app/components/side-button-menu/side-button-menu.component.ts @@ -0,0 +1,36 @@ +import { Component, input, viewChild } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; +import { CommonModule } from '@angular/common'; +import { AddMenuItem } from '../../app.component'; +import { MatTooltip } from '@angular/material/tooltip'; + +@Component({ + selector: 'app-side-button-menu', + imports: [ + MatButtonModule, + MatIconModule, + MatMenuModule, + CommonModule, + MatTooltip, + ], + templateUrl: './side-button-menu.component.html', + styleUrl: './side-button-menu.component.scss', +}) +export class SideButtonMenuComponent { + readonly menuTrigger = viewChild.required('menuTrigger'); + menuItems = input([]); + + focusButton(event: Event) { + event.preventDefault(); + event.stopPropagation(); + const button = document.querySelector( + '.side-add-button' + ) as HTMLButtonElement; + if (button) { + button.focus(); + } + this.menuTrigger().closeMenu(); + } +} diff --git a/modules/ui/src/app/components/simple-dialog/simple-dialog.component.html b/modules/ui/src/app/components/simple-dialog/simple-dialog.component.html index 6d5f768d6..c2201f377 100644 --- a/modules/ui/src/app/components/simple-dialog/simple-dialog.component.html +++ b/modules/ui/src/app/components/simple-dialog/simple-dialog.component.html @@ -13,13 +13,20 @@ See the License for the specific language governing permissions and limitations under the License. --> +{{ + data.icon + }} {{ data.title }}

{{ data.content }}

- + diff --git a/modules/ui/src/app/components/simple-dialog/simple-dialog.component.scss b/modules/ui/src/app/components/simple-dialog/simple-dialog.component.scss index 0d883d76c..924e516f2 100644 --- a/modules/ui/src/app/components/simple-dialog/simple-dialog.component.scss +++ b/modules/ui/src/app/components/simple-dialog/simple-dialog.component.scss @@ -13,31 +13,44 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../theming/colors'; +@use 'colors'; +@use 'variables'; +@use 'mixins'; + +::ng-deep :root { + --mat-dialog-container-max-width: 570px; +} :host { - display: grid; - overflow: hidden; - width: 570px; - padding: 24px 16px 8px 24px; + @include mixins.dialog; + padding: 24px; gap: 10px; } +.simple-dialog-icon { + text-align: center; +} + .simple-dialog-title { - color: #202124; - font-size: 18px; - line-height: 24px; + @include mixins.headline-small(); + padding: 0; + text-align: center; + color: colors.$on-surface; + font-family: variables.$font-primary; } .simple-dialog-content { - font-family: Roboto, sans-serif; + font-family: variables.$font-text; font-size: 14px; line-height: 20px; letter-spacing: 0.2px; - color: $grey-800; + color: colors.$on-surface-variant; } .simple-dialog-actions { padding: 0; min-height: 30px; + button { + font-family: variables.$font-text; + } } diff --git a/modules/ui/src/app/components/simple-dialog/simple-dialog.component.spec.ts b/modules/ui/src/app/components/simple-dialog/simple-dialog.component.spec.ts index b2bdd3fc1..e001f776d 100644 --- a/modules/ui/src/app/components/simple-dialog/simple-dialog.component.spec.ts +++ b/modules/ui/src/app/components/simple-dialog/simple-dialog.component.spec.ts @@ -38,6 +38,8 @@ describe('DeleteFormComponent', () => { useValue: { keydownEvents: () => of(new KeyboardEvent('keydown', { code: '' })), close: () => ({}), + afterOpened: () => of(void 0), + beforeClosed: () => of(void 0), }, }, { provide: MAT_DIALOG_DATA, useValue: {} }, @@ -46,6 +48,7 @@ describe('DeleteFormComponent', () => { fixture = TestBed.createComponent(SimpleDialogComponent); component = fixture.componentInstance; component.data = { + icon: 'favorite', title: 'title?', content: 'content', }; @@ -57,6 +60,12 @@ describe('DeleteFormComponent', () => { expect(component).toBeTruthy(); }); + it('should has icon', () => { + const title = compiled.querySelector('mat-icon') as HTMLElement; + + expect(title.innerHTML).toEqual('favorite'); + }); + it('should has title', () => { const title = compiled.querySelector('.simple-dialog-title') as HTMLElement; diff --git a/modules/ui/src/app/components/simple-dialog/simple-dialog.component.ts b/modules/ui/src/app/components/simple-dialog/simple-dialog.component.ts index af37144e9..de11568c7 100644 --- a/modules/ui/src/app/components/simple-dialog/simple-dialog.component.ts +++ b/modules/ui/src/app/components/simple-dialog/simple-dialog.component.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, Inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogModule, @@ -21,25 +21,46 @@ import { } from '@angular/material/dialog'; import { MatButtonModule } from '@angular/material/button'; import { EscapableDialogComponent } from '../escapable-dialog/escapable-dialog.component'; +import { ComponentWithAnnouncement } from '../component-with-announcement'; +import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { FocusManagerService } from '../../services/focus-manager.service'; +import { MatIconModule } from '@angular/material/icon'; +import { CommonModule } from '@angular/common'; interface DialogData { + icon?: string; title?: string; content?: string; + confirmName?: string; } @Component({ selector: 'app-simple-dialog', templateUrl: './simple-dialog.component.html', styleUrls: ['./simple-dialog.component.scss'], - standalone: true, - imports: [MatDialogModule, MatButtonModule], + + imports: [MatDialogModule, MatButtonModule, MatIconModule, CommonModule], }) -export class SimpleDialogComponent extends EscapableDialogComponent { - constructor( - public override dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: DialogData - ) { - super(dialogRef); +export class SimpleDialogComponent extends ComponentWithAnnouncement( + EscapableDialogComponent +) { + override dialogRef: MatDialogRef; + data: DialogData; + liveAnnouncer: LiveAnnouncer; + override focusService: FocusManagerService; + + constructor() { + const dialogRef = inject>(MatDialogRef); + const data = inject(MAT_DIALOG_DATA); + const liveAnnouncer = inject(LiveAnnouncer); + const focusService = inject(FocusManagerService); + + // @ts-expect-error ComponentWithAnnouncement should have 4 arguments + super(dialogRef, data.title, liveAnnouncer, focusService); + this.dialogRef = dialogRef; + this.data = data; + this.liveAnnouncer = liveAnnouncer; + this.focusService = focusService; } confirm() { diff --git a/modules/ui/src/app/components/snack-bar/snack-bar.component.html b/modules/ui/src/app/components/snack-bar/snack-bar.component.html index 716198299..539623d4b 100644 --- a/modules/ui/src/app/components/snack-bar/snack-bar.component.html +++ b/modules/ui/src/app/components/snack-bar/snack-bar.component.html @@ -15,9 +15,10 @@ -->
-

The Waiting for Device stage is taking more than one minute.

+

It is taking longer than expected to find your device on the network.

- Please check device connection or stop and update system configuration. + Please check the connection to the device or stop and update your system + configuration.

diff --git a/modules/ui/src/app/components/snack-bar/snack-bar.component.scss b/modules/ui/src/app/components/snack-bar/snack-bar.component.scss index c3772e863..cc4ad80a9 100644 --- a/modules/ui/src/app/components/snack-bar/snack-bar.component.scss +++ b/modules/ui/src/app/components/snack-bar/snack-bar.component.scss @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../theming/colors'; +@use 'colors'; +@use 'variables'; .snack-bar-container { display: flex; @@ -22,8 +23,9 @@ margin: 0; } - .snack-bar-actions button.action-btn { - color: $blue-300; + .snack-bar-actions button.action-btn.stop { + font-family: variables.$font-text; + color: colors.$inverse-primary; font-weight: 500; line-height: 20px; letter-spacing: 0.25px; diff --git a/modules/ui/src/app/components/snack-bar/snack-bar.component.ts b/modules/ui/src/app/components/snack-bar/snack-bar.component.ts index c1c4f4242..696a7a46e 100644 --- a/modules/ui/src/app/components/snack-bar/snack-bar.component.ts +++ b/modules/ui/src/app/components/snack-bar/snack-bar.component.ts @@ -27,7 +27,7 @@ import { setIsOpenWaitSnackBar, setIsStopTestrun } from '../../store/actions'; @Component({ selector: 'app-snack-bar', - standalone: true, + imports: [ MatButtonModule, MatSnackBarLabel, @@ -39,8 +39,9 @@ import { setIsOpenWaitSnackBar, setIsStopTestrun } from '../../store/actions'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class SnackBarComponent { + private store = inject>(Store); + snackBarRef = inject(MatSnackBarRef); - constructor(private store: Store) {} wait(): void { this.snackBarRef.dismiss(); diff --git a/modules/ui/src/app/components/spinner/spinner.component.scss b/modules/ui/src/app/components/spinner/spinner.component.scss index 788f2c676..8d582b0dd 100644 --- a/modules/ui/src/app/components/spinner/spinner.component.scss +++ b/modules/ui/src/app/components/spinner/spinner.component.scss @@ -1,5 +1,6 @@ @use '@angular/material' as mat; -@import '../../../theming/colors'; +@use 'm3-theme' as *; +@use 'colors'; .spinner-container { position: absolute; @@ -15,22 +16,24 @@ justify-content: center; } -.loader { - width: 36px; - height: 36px; - border: 4px solid mat.get-color-from-palette($color-primary, 500); - border-bottom-color: transparent; - border-radius: 50%; - display: inline-block; - box-sizing: border-box; - animation: rotation 1s linear infinite; -} - -@keyframes rotation { - 0% { - transform: rotate(0deg); +::ng-deep { + .loader { + width: 36px; + height: 36px; + border: 4px solid mat.get-theme-color($light-theme, primary, 40); + border-bottom-color: transparent; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; } - 100% { - transform: rotate(360deg); + + @keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } } diff --git a/modules/ui/src/app/components/spinner/spinner.component.ts b/modules/ui/src/app/components/spinner/spinner.component.ts index de45b59e9..874b47c6f 100644 --- a/modules/ui/src/app/components/spinner/spinner.component.ts +++ b/modules/ui/src/app/components/spinner/spinner.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { LoaderService } from '../../services/loader.service'; import { CommonModule } from '@angular/common'; import { Observable } from 'rxjs/internal/Observable'; @@ -7,13 +7,13 @@ import { Observable } from 'rxjs/internal/Observable'; selector: 'app-spinner', templateUrl: './spinner.component.html', styleUrls: ['./spinner.component.scss'], - standalone: true, + imports: [CommonModule], }) export class SpinnerComponent implements OnInit { - loader$!: Observable; + loaderService = inject(LoaderService); - constructor(public loaderService: LoaderService) {} + loader$!: Observable; ngOnInit() { this.loader$ = this.loaderService.getLoading(); diff --git a/modules/ui/src/app/components/stepper/stepper-test.component.html b/modules/ui/src/app/components/stepper/stepper-test.component.html new file mode 100644 index 000000000..63570096a --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper-test.component.html @@ -0,0 +1,39 @@ + +
+ + + + Test + + + + + + Test + + + + + +
Header
+
diff --git a/modules/ui/src/app/components/stepper/stepper.component.html b/modules/ui/src/app/components/stepper/stepper.component.html new file mode 100644 index 000000000..46f5e7b73 --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper.component.html @@ -0,0 +1,56 @@ + +
+
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+
diff --git a/modules/ui/src/app/components/stepper/stepper.component.scss b/modules/ui/src/app/components/stepper/stepper.component.scss new file mode 100644 index 000000000..c8ed44e65 --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper.component.scss @@ -0,0 +1,91 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use 'colors'; +@use 'variables'; + +.form-container { + height: 100%; + display: flex; + flex-direction: column; +} + +.form-header { + padding: 24px; +} + +.form-content { + display: grid; + padding: 24px 8px; + gap: 10px; + overflow: hidden; + height: 100%; +} + +.form-footer { + margin-top: auto; + display: inline-block; + text-align: center; + height: 24px; + padding: 0 24px 24px 24px; +} + +.form-steps { + display: inline-flex; + text-align: center; + align-items: center; + height: 100%; +} + +.form-step { + border: 2px solid colors.$lighter-grey; + width: 4px; + height: 4px; + display: inline-block; + border-radius: 100%; + margin: 0 8px; + &.step-active { + border-color: colors.$secondary; + background: colors.$secondary; + } +} + +.form-button-back { + float: left; +} + +.form-button-forward { + float: right; +} + +.form-button-back, +.form-button-forward { + height: variables.$icon-size; + width: variables.$icon-size; + min-width: variables.$icon-size; + margin: 0; + padding: 0; + & mat-icon { + color: colors.$secondary; + width: variables.$icon-size; + height: variables.$icon-size; + font-size: variables.$icon-size; + margin: 0; + } + + &.hidden { + visibility: hidden; + } +} diff --git a/modules/ui/src/app/components/stepper/stepper.component.spec.ts b/modules/ui/src/app/components/stepper/stepper.component.spec.ts new file mode 100644 index 000000000..ed35db415 --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper.component.spec.ts @@ -0,0 +1,103 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StepperComponent } from './stepper.component'; +import { Component, ViewEncapsulation, viewChild, inject } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { CdkStep } from '@angular/cdk/stepper'; +import { MatFormField, MatFormFieldModule } from '@angular/material/form-field'; +import { CommonModule } from '@angular/common'; +import { MatInputModule } from '@angular/material/input'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +@Component({ + selector: 'app-stepper-bypass', + + imports: [ + CdkStep, + StepperComponent, + MatFormField, + CommonModule, + ReactiveFormsModule, + MatInputModule, + MatFormFieldModule, + ], + templateUrl: './stepper-test.component.html', +}) +class TestStepperComponent { + private fb = inject(FormBuilder); + + readonly stepper = viewChild.required('stepper'); + testForm; + firstStep; + secondStep; + constructor() { + this.firstStep = this.fb.group({ + firstControl: ['', [Validators.required]], + }); + this.secondStep = this.fb.group({ + secondControl: ['', [Validators.required]], + }); + this.testForm = this.fb.group({ + steps: this.fb.array([this.firstStep, this.secondStep]), + }); + } +} + +describe('StepperComponent', () => { + let component: TestStepperComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StepperComponent, TestStepperComponent, NoopAnimationsModule], + }) + .overrideComponent(TestStepperComponent, { + set: { encapsulation: ViewEncapsulation.None }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(TestStepperComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have title', () => { + expect(fixture.nativeElement.querySelector('.form-header')).toBeTruthy(); + }); + + it('should not mark selected step touched if not interacted', () => { + component.stepper().nextClick(); + + expect(component.firstStep.touched).toBeFalse(); + }); + + it('should mark selected step touched if interacted', () => { + const button = compiled.querySelector( + '.form-button-forward' + ) as HTMLButtonElement; + button.click(); + + expect(component.firstStep.touched).toBeTrue(); + }); +}); diff --git a/modules/ui/src/app/components/stepper/stepper.component.ts b/modules/ui/src/app/components/stepper/stepper.component.ts new file mode 100644 index 000000000..22321b71b --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper.component.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Input, TemplateRef } from '@angular/core'; +import { CdkStepper, CdkStepperModule } from '@angular/cdk/stepper'; +import { NgForOf, NgIf, NgTemplateOutlet } from '@angular/common'; +import { MatIcon } from '@angular/material/icon'; +import { MatButton } from '@angular/material/button'; +import { FormGroup } from '@angular/forms'; +import { MatTooltipModule } from '@angular/material/tooltip'; + +@Component({ + selector: 'app-stepper', + imports: [ + NgForOf, + NgTemplateOutlet, + CdkStepperModule, + NgIf, + MatIcon, + MatButton, + MatTooltipModule, + ], + templateUrl: './stepper.component.html', + styleUrl: './stepper.component.scss', + providers: [{ provide: CdkStepper, useExisting: StepperComponent }], +}) +export class StepperComponent extends CdkStepper { + @Input() header: TemplateRef | undefined; + @Input() title = ''; + @Input() activeClass = 'active'; + + forwardButtonHidden() { + return this.selectedIndex === this.steps.length - 1; + } + + backButtonHidden() { + return this.selectedIndex === 0; + } + + nextClick() { + if ( + this.selected?.interacted && + !(this.selected?.stepControl as FormGroup)?.valid + ) { + this.selected?.stepControl?.markAllAsTouched(); + } + } + + getStepLabel(isActive: boolean) { + return isActive + ? `Step #${this.selectedIndex + 1} out of ${this.steps.length}` + : ''; + } + + getNavigationLabel(index: number) { + return `Go to step #${index} out of ${this.title}`; + } +} diff --git a/modules/ui/src/app/components/testing-complete/testing-complete.component.spec.ts b/modules/ui/src/app/components/testing-complete/testing-complete.component.spec.ts new file mode 100644 index 000000000..087758dc3 --- /dev/null +++ b/modules/ui/src/app/components/testing-complete/testing-complete.component.spec.ts @@ -0,0 +1,93 @@ +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; + +import { TestingCompleteComponent } from './testing-complete.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Component } from '@angular/core'; +import { MOCK_PROGRESS_DATA_COMPLIANT } from '../../mocks/testrun.mock'; +import { of } from 'rxjs'; +import { MatDialogRef } from '@angular/material/dialog'; +import { + DialogCloseAction, + DownloadZipModalComponent, +} from '../download-zip-modal/download-zip-modal.component'; +import { FocusManagerService } from '../../services/focus-manager.service'; + +describe('TestingCompleteComponent', () => { + let component: TestingCompleteComponent; + let fixture: ComponentFixture; + const mockFocusManagerService = jasmine.createSpyObj( + 'mockFocusManagerService', + ['focusFirstElementInContainer'] + ); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([ + { path: 'risk-assessment', component: FakeRiskAssessmentComponent }, + ]), + TestingCompleteComponent, + BrowserAnimationsModule, + ], + providers: [ + { provide: FocusManagerService, useValue: mockFocusManagerService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestingCompleteComponent); + component = fixture.componentInstance; + component.data = MOCK_PROGRESS_DATA_COMPLIANT; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('#onInit', () => { + it('should focus first element in container when dialog closes with Close action', fakeAsync(() => { + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of({ action: DialogCloseAction.Close }), + } as MatDialogRef); + + component.ngOnInit(); + + tick(1000); + + expect(openSpy).toHaveBeenCalledWith(DownloadZipModalComponent, { + ariaLabel: 'Testing complete', + data: { + profiles: [], + testrunStatus: MOCK_PROGRESS_DATA_COMPLIANT, + isTestingComplete: true, + report: 'https://api.testrun.io/report.pdf', + export: '', + isPilot: false, + }, + autoFocus: 'first-tabbable', + ariaDescribedBy: 'testing-result-main-info', + hasBackdrop: true, + disableClose: true, + panelClass: 'initiate-test-run-dialog', + }); + + tick(1000); + + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalled(); + openSpy.calls.reset(); + })); + }); +}); + +@Component({ + selector: 'app-fake-risk-assessment-component', + template: '', +}) +class FakeRiskAssessmentComponent {} diff --git a/modules/ui/src/app/components/testing-complete/testing-complete.component.ts b/modules/ui/src/app/components/testing-complete/testing-complete.component.ts new file mode 100644 index 000000000..e8d04d149 --- /dev/null +++ b/modules/ui/src/app/components/testing-complete/testing-complete.component.ts @@ -0,0 +1,83 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; +import { Subject, takeUntil, timer } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { + DialogCloseAction, + DialogCloseResult, + DownloadZipModalComponent, +} from '../download-zip-modal/download-zip-modal.component'; +import { Profile } from '../../model/profile'; +import { TestrunStatus } from '../../model/testrun-status'; +import { FocusManagerService } from '../../services/focus-manager.service'; +import { TestingType } from '../../model/device'; + +@Component({ + selector: 'app-testing-complete', + imports: [], + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TestingCompleteComponent implements OnDestroy, OnInit { + dialog = inject(MatDialog); + private focusManagerService = inject(FocusManagerService); + + @Input() profiles: Profile[] = []; + @Input() data!: TestrunStatus | null; + private destroy$: Subject = new Subject(); + + ngOnInit() { + timer(1000) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.openTestingCompleteModal(); + }); + } + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + + private openTestingCompleteModal(): void { + const dialogRef = this.dialog.open(DownloadZipModalComponent, { + ariaLabel: 'Testing complete', + data: { + profiles: this.profiles, + testrunStatus: this.data, + isTestingComplete: true, + report: this.data?.report, + export: this.data?.export, + isPilot: this.data?.device.test_pack === TestingType.Pilot, + }, + autoFocus: 'first-tabbable', + ariaDescribedBy: 'testing-result-main-info', + hasBackdrop: true, + disableClose: true, + panelClass: 'initiate-test-run-dialog', + }); + + dialogRef + ?.afterClosed() + .pipe(takeUntil(this.destroy$)) + .subscribe((result: DialogCloseResult) => { + if (result.action === DialogCloseAction.Close) { + this.focusFirstElement(); + return; + } + }); + } + + private focusFirstElement() { + timer(1000) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(); + }); + } +} diff --git a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.html b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.html index 516e72b49..244c77e88 100644 --- a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.html +++ b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.html @@ -81,28 +81,7 @@

Welcome to Testrun!

-
- - Risk Assessment feature added! -

- Now you can answer a short questionnaire, create a security profile and - attach it to the Testrun result to complete a device verification. Also, - it will speed up the process a lot! -

-

- -

-
-
-
+
Testrun uses Google Analytics to learn about how our users use the application. By installing and running Testrun, you understand and accept @@ -126,7 +105,7 @@

Welcome to Testrun!

(click)="confirm(optOut)" class="confirm-button" color="primary" - mat-raised-button + mat-flat-button aria-label="OK and Proceed to Testrun" type="button"> OK diff --git a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.scss b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.scss index 9eb334496..cd9feedb6 100644 --- a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.scss +++ b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.scss @@ -13,12 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../../theming/colors'; +@use 'colors'; +@use 'mixins'; +@use 'variables'; + +::ng-deep :root { + --mat-dialog-container-max-width: 570px; +} :host { - display: grid; - overflow: hidden; - width: 570px; + @include mixins.dialog; padding: 16px; gap: 16px; } @@ -32,7 +36,7 @@ align-items: start !important; &.check_circle, - &.warning_amber { + &.warning { padding-top: 16px; } } @@ -54,41 +58,121 @@ } .consent-main-content { - padding: 0 66px 16px 66px; - font-family: Roboto, sans-serif; + padding: 0 34px 16px 54px; + font-family: variables.$font-text; font-size: 14px; line-height: 20px; - letter-spacing: 0.2px; - color: $grey-800; + letter-spacing: 0; + color: colors.$on-surface; h2 { - font-size: 14px; + font-size: 16px; + font-weight: 500; + line-height: 24px; } ul { padding-inline-start: 24px; margin-bottom: 0; + + li::marker { + color: colors.$primary; + } } } +::ng-deep .message-link { + color: colors.$primary; + text-decoration: underline; +} + .section-container { + ::ng-deep .callout-context { + padding: 0 0 4px; + } + .section-title { - font-size: 18px; + font-size: 16px; + line-height: 24px; + letter-spacing: 0; + font-weight: 500; + font-family: variables.$font-text; } + + .section-content { + margin: 0; + font-family: variables.$font-text; + } + .section-action-container { text-align: end; - margin-bottom: 0; + margin: 12px 0 0; + } + + .download-link { + color: colors.$orange-40; + font-family: variables.$font-text; + font-size: 14px; + margin-right: -2px; + + ::ng-deep .mat-focus-indicator { + display: none; + } + } +} + +.section-container-pilot { + .section-title { + font-weight: 500; + letter-spacing: 0.25px; + } + .section-content { + margin: 0; + padding-top: 9px; + } +} + +.section-container-info { + ::ng-deep .callout-container { + padding: 10px 16px 14px 16px; + } + + ::ng-deep .callout-icon { + padding: 6px 0; } } .consent-actions { - border-top: 1px solid $lighter-grey; - margin: 0 -16px; - padding: 16px 16px 0 16px; + border-top: 1px solid colors.$outline-variant; + padding: 16px 0 0; + margin: 0; min-height: 30px; justify-content: space-between; } -.consent-actions-opt-out ::ng-deep label { - font-weight: 500; +.consent-actions-opt-out { + ::ng-deep label { + font-family: variables.$font-text; + } + + ::ng-deep .mdc-checkbox__native-control:focus ~ .mat-focus-indicator::before { + content: none; + } + + ::ng-deep + .mdc-checkbox__native-control:focus-visible + ~ .mat-focus-indicator::before { + content: ''; + } +} + +.confirm-button { + border-radius: 12px; + padding: 0 6px; + min-width: 54px; + margin-right: 24px; + + ::ng-deep .mat-focus-indicator { + display: none; + } } diff --git a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.spec.ts b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.spec.ts index c538554f2..9a12c04e1 100644 --- a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.spec.ts +++ b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.spec.ts @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; import { ConsentDialogComponent } from './consent-dialog.component'; import { @@ -24,6 +29,7 @@ import { import { MatButtonModule } from '@angular/material/button'; import { of } from 'rxjs'; import { NEW_VERSION, VERSION } from '../../../mocks/version.mock'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; describe('ConsentDialogComponent', () => { let component: ConsentDialogComponent; @@ -32,7 +38,12 @@ describe('ConsentDialogComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ConsentDialogComponent, MatDialogModule, MatButtonModule], + imports: [ + ConsentDialogComponent, + MatDialogModule, + MatButtonModule, + MatIconTestingModule, + ], providers: [ { provide: MatDialogRef, @@ -46,7 +57,7 @@ describe('ConsentDialogComponent', () => { }); fixture = TestBed.createComponent(ConsentDialogComponent); component = fixture.componentInstance; - component.data = { version: NEW_VERSION, hasRiskProfiles: false }; + component.data = { version: NEW_VERSION, optOut: false }; component.optOut = false; fixture.detectChanges(); compiled = fixture.nativeElement as HTMLElement; @@ -61,7 +72,7 @@ describe('ConsentDialogComponent', () => { const confirmButton = compiled.querySelector( '.confirm-button' ) as HTMLButtonElement; - const dialogRes = { grant: true, isNavigateToRiskAssessment: undefined }; + const dialogRes = { grant: true }; confirmButton?.click(); @@ -77,7 +88,7 @@ describe('ConsentDialogComponent', () => { const confirmButton = compiled.querySelector( '.confirm-button' ) as HTMLButtonElement; - const dialogRes = { grant: false, isNavigateToRiskAssessment: undefined }; + const dialogRes = { grant: false }; confirmButton?.click(); @@ -97,9 +108,25 @@ describe('ConsentDialogComponent', () => { expect(closeSpy).toHaveBeenCalledTimes(0); }); + it('should set focus to first focusable elem when close dialog', fakeAsync(() => { + const button = document.createElement('BUTTON'); + button.classList.add('version-content'); + document.querySelector('body')?.appendChild(button); + + const versionButton = window.document.querySelector( + '.version-content' + ) as HTMLButtonElement; + const buttonFocusSpy = spyOn(versionButton, 'focus'); + + component.confirm(true); + tick(100); + + expect(buttonFocusSpy).toHaveBeenCalled(); + })); + describe('with new version available', () => { beforeEach(() => { - component.data = { version: NEW_VERSION, hasRiskProfiles: false }; + component.data = { version: NEW_VERSION, optOut: false }; fixture.detectChanges(); }); @@ -122,7 +149,7 @@ describe('ConsentDialogComponent', () => { describe('with no new version available', () => { beforeEach(() => { - component.data = { version: VERSION, hasRiskProfiles: false }; + component.data = { version: VERSION, optOut: false }; fixture.detectChanges(); }); @@ -134,51 +161,4 @@ describe('ConsentDialogComponent', () => { expect(content).toBeNull(); }); }); - - describe('with no risk assessment profiles', () => { - beforeEach(() => { - component.data = { version: VERSION, hasRiskProfiles: false }; - fixture.detectChanges(); - }); - - it('should has risk-assessment content', () => { - const content = compiled.querySelector( - '.section-content.risk-assessment' - ) as HTMLElement; - - const innerContent = content.innerHTML.trim(); - expect(innerContent).toContain( - 'Now you can answer a short questionnaire' - ); - }); - - it('should close dialog with isNavigateToRiskAssessment as true when click "confirm"', () => { - const closeSpy = spyOn(component.dialogRef, 'close'); - const riskAssessmentBtn = compiled.querySelector( - '.risk-assessment-button' - ) as HTMLButtonElement; - const dialogRes = { grant: true, isNavigateToRiskAssessment: true }; - - riskAssessmentBtn?.click(); - - expect(closeSpy).toHaveBeenCalledWith(dialogRes); - - closeSpy.calls.reset(); - }); - }); - - describe('with risk assessment profiles', () => { - beforeEach(() => { - component.data = { version: VERSION, hasRiskProfiles: true }; - fixture.detectChanges(); - }); - - it('should not has risk-assessment content', () => { - const content = compiled.querySelector( - '.section-content.risk-assessment' - ) as HTMLElement; - - expect(content).toBeNull(); - }); - }); }); diff --git a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.ts b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.ts index 8b27a8961..63f9cecae 100644 --- a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.ts +++ b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, Inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogModule, @@ -26,15 +26,16 @@ import { CalloutType } from '../../../model/callout-type'; import { NgIf } from '@angular/common'; import { MatCheckbox } from '@angular/material/checkbox'; import { FormsModule } from '@angular/forms'; +import { timer } from 'rxjs'; type DialogData = { version: Version; - hasRiskProfiles: boolean; + optOut: boolean; }; @Component({ selector: 'app-consent-dialog', - standalone: true, + imports: [ MatDialogModule, MatButtonModule, @@ -47,19 +48,25 @@ type DialogData = { styleUrl: './consent-dialog.component.scss', }) export class ConsentDialogComponent { + dialogRef = inject>(MatDialogRef); + data = inject(MAT_DIALOG_DATA); + public readonly CalloutType = CalloutType; - optOut = false; - constructor( - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: DialogData - ) {} + optOut = this.data.optOut; - confirm(optOut: boolean, isNavigateToRiskAssessment?: boolean) { + confirm(optOut: boolean) { // dialog should be closed with opposite value to grant or deny access to GA const dialogResult: ConsentDialogResult = { grant: !optOut, - isNavigateToRiskAssessment, }; this.dialogRef.close(dialogResult); + timer(100).subscribe(() => { + const versionButton = window.document.querySelector( + '.version-content' + ) as HTMLButtonElement; + if (versionButton) { + versionButton.focus(); + } + }); } } diff --git a/modules/ui/src/app/components/version/version.component.html b/modules/ui/src/app/components/version/version.component.html index d45ea69da..2068f6043 100644 --- a/modules/ui/src/app/components/version/version.component.html +++ b/modules/ui/src/app/components/version/version.component.html @@ -18,7 +18,7 @@ mat-button class="version-content" [class.version-content-update]="version.update_available" - [attr.aria-label]="getVersionButtonLabel(version.installed_version)" + [attr.aria-label]="getVersionButtonLabel(version)" (click)="openConsentDialog(version)"> {{ version?.installed_version }} { let component: VersionComponent; @@ -43,7 +48,7 @@ describe('VersionComponent', () => { mockService = jasmine.createSpyObj(['getVersion', 'fetchVersion']); mockService.getVersion.and.returnValue(versionBehaviorSubject$); TestBed.configureTestingModule({ - imports: [VersionComponent], + imports: [VersionComponent, MatIconTestingModule], providers: [{ provide: TestRunService, useValue: mockService }], }); fixture = TestBed.createComponent(VersionComponent); @@ -56,31 +61,32 @@ describe('VersionComponent', () => { }); it('should get correct aria label for version button', () => { - const labelUnavailableVersion = component.getVersionButtonLabel( - UNAVAILABLE_VERSION.installed_version - ); + const labelUnavailableVersion = + component.getVersionButtonLabel(UNAVAILABLE_VERSION); - const labelAvailableVersion = component.getVersionButtonLabel( - VERSION.installed_version - ); + const labelAvailableVersion = component.getVersionButtonLabel(NEW_VERSION); + + const labelVersion = component.getVersionButtonLabel(VERSION); expect(labelUnavailableVersion).toContain( 'Version temporarily unavailable.' ); expect(labelAvailableVersion).toContain('New version is available.'); + expect(labelVersion).toEqual('v1. Click to open the Welcome modal'); }); - it('should open consent window on start', () => { + it('should open consent window on start', fakeAsync(() => { const openSpy = spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(true), - } as MatDialogRef); + afterClosed: () => of({ grant: null }), + } as MatDialogRef); versionBehaviorSubject$.next(VERSION); mockService.getVersion.and.returnValue(versionBehaviorSubject$); fixture.detectChanges(); component.ngOnInit(); + tick(2000); expect(openSpy).toHaveBeenCalled(); - }); + })); it('should open consent window when button clicked', () => { const openSpy = spyOn(component.dialog, 'open').and.returnValue({ diff --git a/modules/ui/src/app/components/version/version.component.ts b/modules/ui/src/app/components/version/version.component.ts index 186c3bcf6..21a6faaed 100644 --- a/modules/ui/src/app/components/version/version.component.ts +++ b/modules/ui/src/app/components/version/version.component.ts @@ -20,6 +20,7 @@ import { OnDestroy, OnInit, Output, + inject, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { @@ -34,31 +35,29 @@ import { tap } from 'rxjs/internal/operators/tap'; import { Observable } from 'rxjs/internal/Observable'; import { Subject } from 'rxjs/internal/Subject'; import { takeUntil } from 'rxjs/internal/operators/takeUntil'; -import { filter } from 'rxjs'; +import { filter, timer } from 'rxjs'; import { ConsentDialogComponent } from './consent-dialog/consent-dialog.component'; +import { LocalStorageService } from '../../services/local-storage.service'; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type declare const gtag: Function; @Component({ selector: 'app-version', - standalone: true, + imports: [CommonModule, MatButtonModule, MatDialogModule], templateUrl: './version.component.html', styleUrls: ['./version.component.scss'], }) export class VersionComponent implements OnInit, OnDestroy { + private testRunService = inject(TestRunService); + private localStorageService = inject(LocalStorageService); + dialog = inject(MatDialog); + @Input() consentShown!: boolean; - @Input() hasRiskProfiles!: boolean; @Output() consentShownEvent = new EventEmitter(); - @Output() navigateToRiskAssessmentEvent = new EventEmitter(); version$!: Observable; private destroy$: Subject = new Subject(); - constructor( - private testRunService: TestRunService, - public dialog: MatDialog - ) {} - ngOnInit() { this.testRunService.fetchVersion(); @@ -66,9 +65,10 @@ export class VersionComponent implements OnInit, OnDestroy { filter(version => version !== null), tap(version => { if (!this.consentShown) { - // @ts-expect-error null is filtered - this.openConsentDialog(version); - this.consentShownEvent.emit(); + timer(2000).subscribe(() => { + this.openConsentDialog(version); + this.consentShownEvent.emit(); + }); } // @ts-expect-error data layer is not null window.dataLayer.push({ @@ -79,14 +79,24 @@ export class VersionComponent implements OnInit, OnDestroy { ); } - getVersionButtonLabel(installedVersion: string): string { - return installedVersion === UNAVAILABLE_VERSION.installed_version - ? 'Version temporarily unavailable. Click to open the Welcome modal' - : `${installedVersion} New version is available. Click to update`; + getVersionButtonLabel(installedVersion: Version): string { + if ( + installedVersion.installed_version === + UNAVAILABLE_VERSION.installed_version + ) { + return 'Version temporarily unavailable. Click to open the Welcome modal'; + } + if (installedVersion.update_available) { + return `${installedVersion.installed_version} New version is available. Click to update`; + } + return `${installedVersion.installed_version}. Click to open the Welcome modal`; } openConsentDialog(version: Version) { - const dialogData = { version, hasRiskProfiles: this.hasRiskProfiles }; + const dialogData = { + version, + optOut: !this.localStorageService.getGAConsent(), + }; const dialogRef = this.dialog.open(ConsentDialogComponent, { ariaLabel: 'Welcome to Testrun modal window', data: dialogData, @@ -106,10 +116,7 @@ export class VersionComponent implements OnInit, OnDestroy { gtag('consent', 'update', { analytics_storage: dialogResult.grant ? 'granted' : 'denied', }); - - if (dialogResult.isNavigateToRiskAssessment) { - this.navigateToRiskAssessmentEvent.emit(); - } + this.localStorageService.setGAConsent(dialogResult.grant); }); } diff --git a/modules/ui/src/app/components/wifi/wifi.component.html b/modules/ui/src/app/components/wifi/wifi.component.html new file mode 100644 index 000000000..a115edfaa --- /dev/null +++ b/modules/ui/src/app/components/wifi/wifi.component.html @@ -0,0 +1,25 @@ + + diff --git a/modules/ui/src/app/components/wifi/wifi.component.spec.ts b/modules/ui/src/app/components/wifi/wifi.component.spec.ts new file mode 100644 index 000000000..55e85a6a7 --- /dev/null +++ b/modules/ui/src/app/components/wifi/wifi.component.spec.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { WifiComponent } from './wifi.component'; + +describe('WifiComponent', () => { + let component: WifiComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [WifiComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(WifiComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('Class tests', () => { + describe('with internet connection', () => { + it('should return label', () => { + expect(component.getLabel(true)).toEqual( + 'Testrun detects a working internet connection for the device under test.' + ); + }); + }); + + describe('with no internet connection', () => { + it('should return label', () => { + expect(component.getLabel(false)).toEqual( + 'No internet connection detected for the device under test.' + ); + }); + }); + + describe('with N/A internet connection', () => { + it('should return label', () => { + expect(component.getLabel(false, true)).toEqual( + 'Internet connection is not being monitored.' + ); + }); + }); + }); + + describe('DOM tests', () => { + describe('with internet connection', () => { + it('should have wifi icon', () => { + component.on = true; + fixture.detectChanges(); + + const icon = compiled.querySelector('mat-icon')?.textContent?.trim(); + + expect(icon).toEqual('wifi'); + }); + }); + + describe('should have no wifi icon', () => { + it('should have no wifi icon', () => { + component.on = false; + fixture.detectChanges(); + + const icon = compiled.querySelector('mat-icon')?.textContent?.trim(); + + expect(icon).toEqual('wifi_off'); + }); + }); + + it('button should be disabled', () => { + component.disable = true; + fixture.detectChanges(); + + const shutdownButton = compiled.querySelector( + '.wifi-button' + ) as HTMLButtonElement; + + expect(shutdownButton?.classList.contains('disabled')).toBeTrue(); + }); + }); +}); diff --git a/modules/ui/src/app/components/wifi/wifi.component.ts b/modules/ui/src/app/components/wifi/wifi.component.ts new file mode 100644 index 000000000..ce741fefd --- /dev/null +++ b/modules/ui/src/app/components/wifi/wifi.component.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Input } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; +import { MatIconButton } from '@angular/material/button'; + +@Component({ + selector: 'app-wifi', + imports: [MatIcon, MatTooltip, MatIconButton], + templateUrl: './wifi.component.html', + styles: ` + .wifi-button.disabled { + opacity: 0.6; + } + `, +}) +export class WifiComponent { + @Input() on: boolean | null = null; + @Input() disable: boolean = false; + + getLabel(on: boolean | null, disable: boolean = false) { + if (disable) { + return 'Internet connection is not being monitored.'; + } + return on + ? 'Testrun detects a working internet connection for the device under test.' + : 'No internet connection detected for the device under test.'; + } +} diff --git a/modules/ui/src/app/guards/can-activate.guard.spec.ts b/modules/ui/src/app/guards/can-activate.guard.spec.ts new file mode 100644 index 000000000..0dd88753f --- /dev/null +++ b/modules/ui/src/app/guards/can-activate.guard.spec.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { Store, StoreModule } from '@ngrx/store'; +import { provideMockStore, MockStore } from '@ngrx/store/testing'; + +import { CanActivateGuard } from './can-activate.guard'; +import { AppState } from '../store/state'; +import { selectSystemConfig } from '../store/selectors'; +import { Routes } from '../model/routes'; + +describe('CanActivateGuard', () => { + let guard: CanActivateGuard; + let store: MockStore; + let router: Router; + + const initialState: AppState = { + hasConnectionSettings: false, + isAllDevicesOutdated: false, + devices: [], + hasDevices: false, + hasExpiredDevices: false, + isOpenAddDevice: false, + riskProfiles: [], + hasRiskProfiles: false, + isStopTestrun: false, + isOpenWaitSnackBar: false, + isOpenStartTestrun: false, + systemStatus: null, + deviceInProgress: null, + status: null, + isTestingComplete: false, + reports: [], + testModules: [], + adapters: {}, + internetConnection: null, + interfaces: {}, + systemConfig: { network: {} }, + isOpenCreateProfile: false, + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [StoreModule.forRoot({})], + providers: [ + CanActivateGuard, + provideMockStore({ initialState }), + { + provide: Router, + useValue: { + navigate: jasmine.createSpy('navigate'), + }, + }, + ], + }); + guard = TestBed.inject(CanActivateGuard); + store = TestBed.inject(Store) as MockStore; + router = TestBed.inject(Router); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); + + it('should navigate to Devices if device_intf is not empty', done => { + const mockConfig = { network: { device_intf: 'eth0' } }; + store.overrideSelector(selectSystemConfig, mockConfig); + + guard.canActivate().subscribe(canActivate => { + expect(canActivate).toBe(false); + expect(router.navigate).toHaveBeenCalledWith([Routes.Devices]); + done(); + }); + }); + + it('should navigate to Settings if device_intf is an empty string', done => { + const mockConfig = { network: { device_intf: '' } }; + store.overrideSelector(selectSystemConfig, mockConfig); + + guard.canActivate().subscribe(canActivate => { + expect(canActivate).toBe(false); + expect(router.navigate).toHaveBeenCalledWith([Routes.Settings]); + done(); + }); + }); +}); diff --git a/modules/ui/src/app/guards/can-activate.guard.ts b/modules/ui/src/app/guards/can-activate.guard.ts new file mode 100644 index 000000000..9953453af --- /dev/null +++ b/modules/ui/src/app/guards/can-activate.guard.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { inject, Injectable } from '@angular/core'; +import { CanActivate, Router } from '@angular/router'; +import { Observable, of } from 'rxjs'; +import { switchMap, catchError, tap, filter, take } from 'rxjs/operators'; +import { AppState } from '../store/state'; +import { Store } from '@ngrx/store'; +import { selectSystemConfig } from '../store/selectors'; +import { Routes } from '../model/routes'; + +@Injectable({ + providedIn: 'root', +}) +export class CanActivateGuard implements CanActivate { + private store = inject>(Store); + private readonly router = inject(Router); + + canActivate(): Observable { + return this.store.select(selectSystemConfig).pipe( + filter(config => config.network?.device_intf !== undefined), + tap(config => { + if (config.network?.device_intf === '') { + this.router.navigate([Routes.Settings]); + } else { + this.router.navigate([Routes.Devices]); + } + }), + take(1), + switchMap(() => of(false)), + catchError(() => { + this.router.navigate([Routes.Settings]); + return of(false); + }) + ); + } +} diff --git a/modules/ui/src/app/pages/devices/devices-routing.module.ts b/modules/ui/src/app/guards/can-deactivate.guard.spec.ts similarity index 61% rename from modules/ui/src/app/pages/devices/devices-routing.module.ts rename to modules/ui/src/app/guards/can-deactivate.guard.spec.ts index 19acb07c9..5571654bc 100644 --- a/modules/ui/src/app/pages/devices/devices-routing.module.ts +++ b/modules/ui/src/app/guards/can-deactivate.guard.spec.ts @@ -13,14 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { DevicesComponent } from './devices.component'; +import { TestBed } from '@angular/core/testing'; -const routes: Routes = [{ path: '', component: DevicesComponent }]; +import { CanDeactivateGuard } from './can-deactivate.guard'; -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class DevicesRoutingModule {} +describe('CanDeactivateGuard', () => { + let guard: CanDeactivateGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(CanDeactivateGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/modules/ui/src/app/guards/can-deactivate.guard.ts b/modules/ui/src/app/guards/can-deactivate.guard.ts new file mode 100644 index 000000000..68fb2147c --- /dev/null +++ b/modules/ui/src/app/guards/can-deactivate.guard.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { CanDeactivate, UrlTree } from '@angular/router'; +import { Observable } from 'rxjs'; + +type CanDeactivateType = + | Observable + | Promise + | boolean + | UrlTree; + +export interface CanComponentDeactivate { + canDeactivate: () => CanDeactivateType; +} +@Injectable({ + providedIn: 'root', +}) +export class CanDeactivateGuard + implements CanDeactivate +{ + canDeactivate(component: CanComponentDeactivate): CanDeactivateType { + return component.canDeactivate ? component.canDeactivate() : true; + } +} diff --git a/modules/ui/src/app/interceptors/error-handler.interceptor.ts b/modules/ui/src/app/interceptors/error-handler.interceptor.ts index 07b1fb3f7..573c5f379 100644 --- a/modules/ui/src/app/interceptors/error-handler.interceptor.ts +++ b/modules/ui/src/app/interceptors/error-handler.interceptor.ts @@ -15,10 +15,9 @@ */ import { ErrorHandler, - Inject, Injectable, InjectionToken, - Optional, + inject, } from '@angular/core'; export const WINDOW_TOKEN = new InjectionToken('Window'); @@ -27,16 +26,14 @@ export const WINDOW_TOKEN = new InjectionToken('Window'); */ @Injectable() export class ErrorHandlerInterceptor implements ErrorHandler { - constructor( - @Optional() - @Inject(WINDOW_TOKEN) - private window: Window = window - ) {} + private _window = inject(WINDOW_TOKEN, { optional: true }) ?? window; + + constructor() {} handleError(error: Error): void { const chunkFailedMessage = /Loading chunk [\d]+ failed/; if (chunkFailedMessage.test(error.message)) { - this.window.location.reload(); + this._window.location.reload(); } } } diff --git a/modules/ui/src/app/interceptors/error.interceptor.spec.ts b/modules/ui/src/app/interceptors/error.interceptor.spec.ts index 9fff32863..7271223a2 100644 --- a/modules/ui/src/app/interceptors/error.interceptor.spec.ts +++ b/modules/ui/src/app/interceptors/error.interceptor.spec.ts @@ -42,11 +42,15 @@ describe('ErrorInterceptor', () => { interceptor = TestBed.inject(ErrorInterceptor); }); + afterEach(() => { + notificationServiceMock.notify.calls.reset(); + }); + it('should be created', () => { expect(interceptor).toBeTruthy(); }); - it('should notify about backend errors', done => { + it('should notify about backend errors with message if exist', done => { const next: HttpHandler = { handle: () => { return throwError( @@ -66,6 +70,26 @@ describe('ErrorInterceptor', () => { ); }); + it('should notify about backend errors with default message', done => { + const next: HttpHandler = { + handle: () => { + return throwError(new HttpErrorResponse({ status: 500 })); + }, + }; + + const requestMock = new HttpRequest('GET', '/test'); + + interceptor.intercept(requestMock, next).subscribe( + () => ({}), + () => { + expect(notificationServiceMock.notify).toHaveBeenCalledWith( + 'Something went wrong. Check the Terminal for details.' + ); + done(); + } + ); + }); + it('should notify about other errors', done => { const next: HttpHandler = { handle: () => { @@ -79,7 +103,7 @@ describe('ErrorInterceptor', () => { () => ({}), () => { expect(notificationServiceMock.notify).toHaveBeenCalledWith( - 'Back End is not responding. Please, try again later.' + 'Testrun is not responding. Please try again in a moment.' ); done(); } @@ -99,7 +123,7 @@ describe('ErrorInterceptor', () => { () => ({}), () => { expect(notificationServiceMock.notify).toHaveBeenCalledWith( - 'Back End is not responding. Please, try again later.' + 'Testrun is not responding. Please try again in a moment.' ); done(); } diff --git a/modules/ui/src/app/interceptors/error.interceptor.ts b/modules/ui/src/app/interceptors/error.interceptor.ts index 924cbde02..536728331 100644 --- a/modules/ui/src/app/interceptors/error.interceptor.ts +++ b/modules/ui/src/app/interceptors/error.interceptor.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpErrorResponse, HttpEvent, @@ -30,25 +30,30 @@ import { } from 'rxjs'; import { NotificationService } from '../services/notification.service'; -import { SYSTEM_STOP } from '../services/test-run.service'; +import { SYSTEM_STOP, EXPORT } from '../services/test-run.service'; import { finalize } from 'rxjs/operators'; const DEFAULT_TIMEOUT_MS = 5000; const SYSTEM_STOP_TIMEOUT_MS = 60 * 1000; +const EXPORT_ZIP_TIMEOUT_MS = 60 * 1000; @Injectable() export class ErrorInterceptor implements HttpInterceptor { + private notificationService = inject(NotificationService); + private isTestrunStop = false; - constructor(private notificationService: NotificationService) {} intercept( request: HttpRequest, next: HttpHandler, timeoutMs = DEFAULT_TIMEOUT_MS ): Observable> { - const timeoutValue = - request.url.includes(SYSTEM_STOP) || this.isTestrunStop - ? SYSTEM_STOP_TIMEOUT_MS - : timeoutMs; + let timeoutValue = timeoutMs; + if (request.url.includes(SYSTEM_STOP) || this.isTestrunStop) { + timeoutValue = SYSTEM_STOP_TIMEOUT_MS; + } + if (request.url.includes(EXPORT)) { + timeoutValue = EXPORT_ZIP_TIMEOUT_MS; + } if (request.url.includes(SYSTEM_STOP)) { this.isTestrunStop = true; } @@ -57,17 +62,19 @@ export class ErrorInterceptor implements HttpInterceptor { catchError((error: HttpErrorResponse | TimeoutError) => { if (error instanceof TimeoutError) { this.notificationService.notify( - 'Back End is not responding. Please, try again later.' + 'Testrun is not responding. Please try again in a moment.' ); } else { if (error.status === 0) { this.notificationService.notify( - 'Back End is not responding. Please, try again later.' + 'Testrun is not responding. Please try again in a moment.' ); } else { this.notificationService.notify( - error.error?.error || error.message + error.error?.error || + 'Something went wrong. Check the Terminal for details.' ); + console.error(error.error?.error || error.message); } } return throwError(error); diff --git a/modules/ui/src/app/interceptors/loading.interceptor.ts b/modules/ui/src/app/interceptors/loading.interceptor.ts index c49ea4bc6..3129e0d4d 100644 --- a/modules/ui/src/app/interceptors/loading.interceptor.ts +++ b/modules/ui/src/app/interceptors/loading.interceptor.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Injectable, NgZone } from '@angular/core'; +import { Injectable, NgZone, inject } from '@angular/core'; import { HttpEvent, HttpHandler, @@ -26,12 +26,10 @@ import { LoaderService } from '../services/loader.service'; @Injectable() export class LoadingInterceptor implements HttpInterceptor { - private totalRequests = 0; + private loadingService = inject(LoaderService); + private zone = inject(NgZone); - constructor( - private loadingService: LoaderService, - private zone: NgZone - ) {} + private totalRequests = 0; intercept( request: HttpRequest, diff --git a/modules/ui/src/app/mocks/device.mock.ts b/modules/ui/src/app/mocks/device.mock.ts index 6066593e6..19732c4c3 100644 --- a/modules/ui/src/app/mocks/device.mock.ts +++ b/modules/ui/src/app/mocks/device.mock.ts @@ -13,9 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Device } from '../model/device'; +import { Device, DeviceStatus } from '../model/device'; +import { ProfileRisk } from '../model/profile'; +import { FormControlType, QuestionFormat } from '../model/question'; export const device = { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: 'O3-DIN-CPU', + mac_addr: '00:1e:42:35:73:c4', + test_modules: { + dns: { + enabled: true, + }, + }, +} as Device; + +export const expired_device = { + status: DeviceStatus.INVALID, manufacturer: 'Delta', model: 'O3-DIN-CPU', mac_addr: '00:1e:42:35:73:c4', @@ -26,6 +41,7 @@ export const device = { }, } as Device; export const updated_device = { + status: DeviceStatus.VALID, manufacturer: 'Alpha', model: 'O3-XYZ-CPU', mac_addr: '00:1e:42:35:73:11', @@ -43,8 +59,53 @@ export const MOCK_TEST_MODULES = [ enabled: true, }, { - displayName: 'Smart Ready', + displayName: 'Udmi', name: 'udmi', - enabled: false, + enabled: true, + }, +]; + +export const MOCK_MODULES = ['Connection', 'Udmi']; + +export const DEVICES_FORM: QuestionFormat[] = [ + { + question: 'What type of device is this?', + type: FormControlType.SELECT, + options: [ + { + text: 'Building Automation Gateway', + risk: ProfileRisk.HIGH, + id: 1, + }, + { + text: 'IoT Gateway', + risk: ProfileRisk.LIMITED, + id: 2, + }, + ], + }, + { + question: 'Does your device process any sensitive information? ', + type: FormControlType.SELECT, + options: [ + { + id: 1, + text: 'Yes', + risk: ProfileRisk.LIMITED, + }, + { + id: 2, + text: 'No', + risk: ProfileRisk.HIGH, + }, + ], + }, + { + question: 'Please select the technology this device falls into', + type: FormControlType.SELECT, + options: [ + { text: 'Hardware - Access Control' }, + { text: 'Hardware - Air quality' }, + ], }, ]; diff --git a/modules/ui/src/app/mocks/profile.mock.ts b/modules/ui/src/app/mocks/profile.mock.ts index 3715685cd..749db8fb7 100644 --- a/modules/ui/src/app/mocks/profile.mock.ts +++ b/modules/ui/src/app/mocks/profile.mock.ts @@ -14,12 +14,8 @@ * limitations under the License. */ -import { - FormControlType, - Profile, - ProfileFormat, - ProfileStatus, -} from '../model/profile'; +import { Profile, ProfileFormat, ProfileStatus } from '../model/profile'; +import { FormControlType } from '../model/question'; export const PROFILE_MOCK: Profile = { name: 'Primary profile', @@ -63,7 +59,7 @@ export const PROFILE_MOCK_3: Profile = { export const PROFILE_FORM: ProfileFormat[] = [ { - question: 'Email', + question: 'What is the email of the device owner(s)?', type: FormControlType.EMAIL_MULTIPLE, validation: { required: true, @@ -112,7 +108,10 @@ export const NEW_PROFILE_MOCK = { status: ProfileStatus.VALID, name: 'New profile', questions: [ - { question: 'Email', answer: 'a@test.te;b@test.te, c@test.te' }, + { + question: 'What is the email of the device owner(s)?', + answer: 'a@test.te;b@test.te, c@test.te', + }, { question: 'What type of device do you need reviewed?', answer: 'test', @@ -133,7 +132,7 @@ export const NEW_PROFILE_MOCK_DRAFT = { status: ProfileStatus.DRAFT, name: 'New profile', questions: [ - { question: 'Email', answer: '' }, + { question: 'What is the email of the device owner(s)?', answer: '' }, { question: 'What type of device do you need reviewed?', answer: '', @@ -155,3 +154,85 @@ export const RENAME_PROFILE_MOCK = { name: 'Primary profile', rename: 'New profile', }; + +export const COPY_PROFILE_MOCK: Profile = { + name: 'Copy of Primary profile', + status: ProfileStatus.VALID, + created: '2025-05-23 12:38:26', + questions: [ + { + question: 'What is the email of the device owner(s)?', + answer: 'boddey@google.com, cmeredith@google.com', + }, + { + question: 'What type of device do you need reviewed?', + answer: 'IoT Sensor', + }, + { + question: 'Are any of the following statements true about your device?', + answer: 'First', + }, + { + question: 'What features does the device have?', + answer: [0, 1, 2], + }, + { + question: 'Comments', + answer: 'Yes', + }, + ], +}; + +export const DRAFT_COPY_PROFILE_MOCK: Profile = { + name: 'Copy of Primary profile', + status: ProfileStatus.DRAFT, + questions: [ + { + question: 'What is the email of the device owner(s)?', + answer: 'boddey@google.com, cmeredith@google.com', + }, + { + question: 'What type of device do you need reviewed?', + answer: 'IoT Sensor', + }, + { + question: 'Are any of the following statements true about your device?', + answer: 'First', + }, + { + question: 'What features does the device have?', + answer: [0, 1, 2], + }, + { + question: 'Comments', + answer: 'Yes', + }, + ], +}; + +export const OUTDATED_DRAFT_PROFILE_MOCK: Profile = { + name: 'Outdated profile', + status: ProfileStatus.DRAFT, + questions: [ + { + question: 'Old question', + answer: 'qwerty', + }, + { + question: 'What is the email of the device owner(s)?', + answer: '', + }, + { + question: 'What type of device do you need reviewed?', + answer: 'IoT Sensor', + }, + { + question: 'Another old question', + answer: 'qwerty', + }, + ], +}; + +export const EXPIRED_PROFILE_MOCK: Profile = Object.assign({}, PROFILE_MOCK, { + status: ProfileStatus.EXPIRED, +}); diff --git a/modules/ui/src/app/mocks/reports.mock.ts b/modules/ui/src/app/mocks/reports.mock.ts index 0cfb39420..5cfff1e06 100644 --- a/modules/ui/src/app/mocks/reports.mock.ts +++ b/modules/ui/src/app/mocks/reports.mock.ts @@ -1,88 +1,174 @@ import { HistoryTestrun, TestrunStatus } from '../model/testrun-status'; import { MatTableDataSource } from '@angular/material/table'; +import { DeviceStatus, TestingType } from '../model/device'; export const HISTORY = [ { mac_addr: '01:02:03:04:05:06', - status: 'compliant', + status: 'Complete', + result: 'Compliant', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-SRC', mac_addr: '01:02:03:04:05:06', firmware: '1.2.2', + test_pack: TestingType.Qualification, }, + tags: [], report: 'https://api.testrun.io/report.pdf', + export: 'https://api.testrun.io/export.pdf', started: '2023-06-23T10:11:00.123Z', finished: '2023-06-23T10:17:10.123Z', }, { - status: 'compliant', + status: 'Complete', + result: 'Compliant', mac_addr: '01:02:03:04:05:07', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-SRC', mac_addr: '01:02:03:04:05:07', firmware: '1.2.3', + test_pack: TestingType.Qualification, }, + tags: [], report: 'https://api.testrun.io/report.pdf', + export: 'https://api.testrun.io/export.pdf', started: '2023-07-23T10:11:00.123Z', finished: '2023-07-23T10:17:10.123Z', }, + { + mac_addr: null, + status: 'Complete', + result: 'Compliant', + device: { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: '03-DIN-SRC', + mac_addr: '01:02:03:04:05:08', + firmware: '1.2.2', + test_pack: TestingType.Qualification, + }, + tags: [], + report: 'https://api.testrun.io/report.pdf', + export: 'https://api.testrun.io/export.pdf', + started: '2023-06-23T10:11:00.123Z', + finished: '2023-06-23T10:17:10.123Z', + }, ] as TestrunStatus[]; export const HISTORY_AFTER_REMOVE = [ { mac_addr: '01:02:03:04:05:06', - status: 'compliant', + status: 'Complete', + result: 'Compliant', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-SRC', mac_addr: '01:02:03:04:05:06', firmware: '1.2.2', + test_pack: TestingType.Qualification, }, + tags: [], report: 'https://api.testrun.io/report.pdf', + export: 'https://api.testrun.io/export.pdf', started: '2023-06-23T10:11:00.123Z', finished: '2023-06-23T10:17:10.123Z', - deviceFirmware: '1.2.2', - deviceInfo: 'Delta 03-DIN-SRC', - duration: '06m 10s', }, -]; + { + mac_addr: null, + status: 'Complete', + result: 'Compliant', + device: { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: '03-DIN-SRC', + mac_addr: '01:02:03:04:05:08', + firmware: '1.2.2', + test_pack: TestingType.Qualification, + }, + tags: [], + report: 'https://api.testrun.io/report.pdf', + export: 'https://api.testrun.io/export.pdf', + started: '2023-06-23T10:11:00.123Z', + finished: '2023-06-23T10:17:10.123Z', + }, +] as TestrunStatus[]; export const FORMATTED_HISTORY = [ { - status: 'compliant', + status: 'Complete', + result: 'Compliant', mac_addr: '01:02:03:04:05:06', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-SRC', mac_addr: '01:02:03:04:05:06', firmware: '1.2.2', + test_pack: TestingType.Qualification, }, + tags: [], report: 'https://api.testrun.io/report.pdf', + export: 'https://api.testrun.io/export.pdf', started: '2023-06-23T10:11:00.123Z', finished: '2023-06-23T10:17:10.123Z', deviceFirmware: '1.2.2', deviceInfo: 'Delta 03-DIN-SRC', + testResult: 'Compliant', duration: '06m 10s', + program: 'Device Qualification', }, { - status: 'compliant', + status: 'Complete', + result: 'Compliant', mac_addr: '01:02:03:04:05:07', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-SRC', mac_addr: '01:02:03:04:05:07', firmware: '1.2.3', + test_pack: TestingType.Qualification, }, + tags: [], report: 'https://api.testrun.io/report.pdf', + export: 'https://api.testrun.io/export.pdf', started: '2023-07-23T10:11:00.123Z', finished: '2023-07-23T10:17:10.123Z', deviceFirmware: '1.2.3', deviceInfo: 'Delta 03-DIN-SRC', + testResult: 'Compliant', + duration: '06m 10s', + program: 'Device Qualification', + }, + { + mac_addr: null, + status: 'Complete', + result: 'Compliant', + device: { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: '03-DIN-SRC', + mac_addr: '01:02:03:04:05:08', + firmware: '1.2.2', + test_pack: TestingType.Qualification, + }, + tags: [], + report: 'https://api.testrun.io/report.pdf', + export: 'https://api.testrun.io/export.pdf', + started: '2023-06-23T10:11:00.123Z', + finished: '2023-06-23T10:17:10.123Z', + deviceFirmware: '1.2.2', + deviceInfo: 'Delta 03-DIN-SRC', + testResult: 'Compliant', duration: '06m 10s', + program: 'Device Qualification', }, -]; +] as HistoryTestrun[]; export const FILTERS = { deviceInfo: 'test', diff --git a/modules/ui/src/app/mocks/settings.mock.ts b/modules/ui/src/app/mocks/settings.mock.ts index baab9a2c0..53f092a0b 100644 --- a/modules/ui/src/app/mocks/settings.mock.ts +++ b/modules/ui/src/app/mocks/settings.mock.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SystemConfig, SystemInterfaces } from '../model/setting'; +import { Adapters, SystemConfig, SystemInterfaces } from '../model/setting'; export const MOCK_SYSTEM_CONFIG_WITH_NO_DATA: SystemConfig = { network: { @@ -33,16 +33,21 @@ export const MOCK_SYSTEM_CONFIG_WITH_DATA: SystemConfig = { monitor_period: 600, }; -export const MOCK_INTERFACES: SystemInterfaces = { - mockDeviceKey: 'mockDeviceValue', - mockInternetKey: 'mockInternetValue', +export const MOCK_SYSTEM_CONFIG_WITH_SINGLE_PORT: SystemConfig = { + network: { + device_intf: 'mockDeviceKey', + internet_intf: '', + }, + log_level: 'DEBUG', + monitor_period: 600, + single_intf: true, }; -export const MOCK_INTERNET_OPTIONS: SystemInterfaces = { - '': 'Not specified', +export const MOCK_INTERFACES: SystemInterfaces = { mockDeviceKey: 'mockDeviceValue', mockInternetKey: 'mockInternetValue', }; + export const MOCK_DEVICE_VALUE: SystemInterfaces = { key: 'mockDeviceKey', value: 'mockDeviceValue', @@ -60,3 +65,8 @@ export const MOCK_PERIOD_VALUE: SystemInterfaces = { key: '600', value: 'Very slow device', }; + +export const MOCK_ADAPTERS: Adapters = { + adapters_added: { mockNewInternetKey: 'mockNewInternetValue' }, + adapters_removed: { mockInternetKey: 'mockInternetValue' }, +}; diff --git a/modules/ui/src/app/mocks/testrun.mock.ts b/modules/ui/src/app/mocks/testrun.mock.ts index 0572e79c0..188e9170c 100644 --- a/modules/ui/src/app/mocks/testrun.mock.ts +++ b/modules/ui/src/app/mocks/testrun.mock.ts @@ -15,22 +15,33 @@ */ import { IResult, + RequiredResult, + ResultOfTestrun, StatusOfTestrun, TestrunStatus, TestsData, } from '../model/testrun-status'; +import { DeviceStatus } from '../model/device'; export const TEST_DATA_RESULT: IResult[] = [ { name: 'dns.network.hostname_resolution', description: 'The device should resolve hostnames', result: 'Compliant', + required_result: RequiredResult.Required, }, { name: 'dns.network.from_dhcp', description: 'The device should use the DNS server provided by the DHCP server', result: 'Non-Compliant', + required_result: RequiredResult.Informational, + }, + { + name: 'dns.mdns', + description: 'Does the device has MDNS (or any kind of IP multicast)', + result: 'Not Started', + required_result: RequiredResult.RequiredIfApplicable, }, ]; @@ -44,12 +55,30 @@ export const TEST_DATA_RESULT_WITH_RECOMMENDATIONS: IResult[] = [ 'An example of a step to resolve', 'Disable any running NTP server', ], + required_result: RequiredResult.Required, }, ]; -export const TEST_DATA_TABLE_RESULT: IResult[] = [ - ...TEST_DATA_RESULT, - ...new Array(24).fill(null).map(() => ({}) as IResult), +export const TEST_DATA_RESULT_WITH_ERROR: IResult[] = [ + { + name: 'dns.network.hostname_resolution', + description: 'The device should resolve hostnames', + result: 'Compliant', + required_result: RequiredResult.Required, + }, + { + name: 'dns.network.from_dhcp', + description: + 'The device should use the DNS server provided by the DHCP server', + result: 'Error', + required_result: RequiredResult.Required, + }, + { + name: 'dns.mdns', + description: 'Does the device has MDNS (or any kind of IP multicast)', + result: 'Not Started', + required_result: RequiredResult.Required, + }, ]; export const EMPTY_RESULT = new Array(100) @@ -62,15 +91,17 @@ export const TEST_DATA: TestsData = { }; const PROGRESS_DATA_RESPONSE = ( - status: string, + status: StatusOfTestrun, finished: string | null, tests: TestsData | IResult[], - report?: string + report: string = '', + result?: ResultOfTestrun ) => { - return { + const response = { status, mac_addr: '01:02:03:04:05:06', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-CPU', mac_addr: '01:02:03:04:05:06', @@ -80,7 +111,13 @@ const PROGRESS_DATA_RESPONSE = ( finished, tests, report, - }; + export: '', + tags: ['VSA', 'Other tag', 'And one more'], + } as TestrunStatus; + if (result) { + response.result = result; + } + return response; }; export const MOCK_PROGRESS_DATA_CANCELLING: TestrunStatus = @@ -91,20 +128,30 @@ export const MOCK_PROGRESS_DATA_IN_PROGRESS_EMPTY: TestrunStatus = PROGRESS_DATA_RESPONSE(StatusOfTestrun.InProgress, null, []); export const MOCK_PROGRESS_DATA_COMPLIANT: TestrunStatus = PROGRESS_DATA_RESPONSE( - StatusOfTestrun.Compliant, + StatusOfTestrun.Complete, '2023-06-22T09:20:00.123Z', TEST_DATA_RESULT, - 'https://api.testrun.io/report.pdf' + 'https://api.testrun.io/report.pdf', + ResultOfTestrun.Compliant ); export const MOCK_PROGRESS_DATA_NON_COMPLIANT: TestrunStatus = PROGRESS_DATA_RESPONSE( - StatusOfTestrun.NonCompliant, + StatusOfTestrun.Complete, '2023-06-22T09:20:00.123Z', TEST_DATA_RESULT, - 'https://api.testrun.io/report.pdf' + 'https://api.testrun.io/report.pdf', + ResultOfTestrun.NonCompliant ); +export const MOCK_PROGRESS_DATA_PROCEED: TestrunStatus = PROGRESS_DATA_RESPONSE( + StatusOfTestrun.Proceed, + '2023-06-22T09:20:00.123Z', + TEST_DATA_RESULT, + 'https://api.testrun.io/report.pdf', + ResultOfTestrun.Compliant +); + export const MOCK_PROGRESS_DATA_CANCELLED: TestrunStatus = PROGRESS_DATA_RESPONSE(StatusOfTestrun.Cancelled, null, TEST_DATA); @@ -131,3 +178,22 @@ export const MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE: TestrunStatus = { status: StatusOfTestrun.WaitingForDevice, started: null, }; + +export const MOCK_PROGRESS_DATA_STARTING: TestrunStatus = { + ...MOCK_PROGRESS_DATA_IN_PROGRESS, + status: StatusOfTestrun.Starting, + started: null, +}; + +export const MOCK_PROGRESS_DATA_VALIDATING: TestrunStatus = { + ...MOCK_PROGRESS_DATA_IN_PROGRESS, + status: StatusOfTestrun.Validating, + started: null, +}; + +export const MOCK_PROGRESS_DATA_WITH_ERROR: TestrunStatus = + PROGRESS_DATA_RESPONSE(StatusOfTestrun.InProgress, null, { + ...TEST_DATA, + total: 3, + results: TEST_DATA_RESULT_WITH_ERROR, + }); diff --git a/modules/ui/src/app/mocks/topic.mock.ts b/modules/ui/src/app/mocks/topic.mock.ts new file mode 100644 index 000000000..4309ae84f --- /dev/null +++ b/modules/ui/src/app/mocks/topic.mock.ts @@ -0,0 +1,5 @@ +import { InternetConnection } from '../model/topic'; + +export const MOCK_INTERNET: InternetConnection = { + connection: false, +}; diff --git a/modules/ui/src/app/model/callout-type.ts b/modules/ui/src/app/model/callout-type.ts index c784b46f6..2e2707066 100644 --- a/modules/ui/src/app/model/callout-type.ts +++ b/modules/ui/src/app/model/callout-type.ts @@ -15,8 +15,8 @@ */ export enum CalloutType { Info = 'info', + InfoPilot = 'info pilot', Check = 'check_circle', - Warning = 'warning_amber', + Warning = 'warning', Error = 'error', - ErrorOutline = 'error_outline', } diff --git a/modules/ui/src/app/model/certificate.ts b/modules/ui/src/app/model/certificate.ts index ecf47ca84..0a36e1300 100644 --- a/modules/ui/src/app/model/certificate.ts +++ b/modules/ui/src/app/model/certificate.ts @@ -15,7 +15,7 @@ */ export interface Certificate { name: string; - status?: string; + status?: CertificateStatus; organisation?: string; expires?: string; uploading?: boolean; diff --git a/modules/ui/src/app/model/device.ts b/modules/ui/src/app/model/device.ts index ba526e661..62100f170 100644 --- a/modules/ui/src/app/model/device.ts +++ b/modules/ui/src/app/model/device.ts @@ -13,12 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Question } from './profile'; + export interface Device { manufacturer: string; model: string; mac_addr: string; test_modules?: TestModules; firmware?: string; + status?: DeviceStatus; + type?: string; + technology?: string; + test_pack?: TestingType; + additional_info?: Question[]; +} + +export enum DeviceStatus { + VALID = 'Valid', + INVALID = 'Invalid', } /** @@ -43,3 +55,13 @@ export enum DeviceView { Basic = 'basic', WithActions = 'with actions', } + +export enum TestingType { + Pilot = 'Pilot Assessment', + Qualification = 'Device Qualification', +} + +export enum DeviceAction { + StartNewTestrun = 'Start new Testrun', + Delete = 'Delete', +} diff --git a/modules/ui/src/app/components/shutdown-app/shutdown-app.component.scss b/modules/ui/src/app/model/entity-action.ts similarity index 69% rename from modules/ui/src/app/components/shutdown-app/shutdown-app.component.scss rename to modules/ui/src/app/model/entity-action.ts index 8c6a76e74..17e3062f1 100644 --- a/modules/ui/src/app/components/shutdown-app/shutdown-app.component.scss +++ b/modules/ui/src/app/model/entity-action.ts @@ -13,16 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { - ::ng-deep.mat-mdc-icon-button .mat-mdc-button-persistent-ripple { - border-radius: inherit; - } +export interface EntityAction { + action: string; + icon?: string; + svgIcon?: string; } -.shutdown-button { - border-radius: 20px; - padding: 0; - box-sizing: border-box; - height: 34px; - margin: 11px 2px 11px 0; - line-height: 50% !important; + +export interface EntityActionResult { + action: string; + entity: T; + index: number; } diff --git a/modules/ui/src/app/model/filters.ts b/modules/ui/src/app/model/filters.ts index 15144349e..b85ebbc52 100644 --- a/modules/ui/src/app/model/filters.ts +++ b/modules/ui/src/app/model/filters.ts @@ -21,6 +21,13 @@ export enum FilterName { DateRange = 'dateRange', } +export enum FilterTitle { + DeviceInfo = 'Enter device name', + DeviceFirmware = 'Enter firmware name', + Results = 'Select status', + Started = 'Select dates', +} + export interface ReportFilters { deviceInfo: string; deviceFirmware: string; diff --git a/modules/ui/src/app/pages/reports/reports-routing.module.ts b/modules/ui/src/app/model/layout-type.ts similarity index 63% rename from modules/ui/src/app/pages/reports/reports-routing.module.ts rename to modules/ui/src/app/model/layout-type.ts index d5542172f..736e1ae3b 100644 --- a/modules/ui/src/app/pages/reports/reports-routing.module.ts +++ b/modules/ui/src/app/model/layout-type.ts @@ -13,14 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { ReportsComponent } from './reportscomponent'; -const routes: Routes = [{ path: '', component: ReportsComponent }]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class ReportsRoutingModule {} +export enum LayoutType { + Device = 'Devices', + Profile = 'Risk Assessment', +} diff --git a/modules/ui/src/app/model/profile.ts b/modules/ui/src/app/model/profile.ts index efdb779e6..fcf2f8207 100644 --- a/modules/ui/src/app/model/profile.ts +++ b/modules/ui/src/app/model/profile.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { QuestionFormat } from './question'; export interface Profile { name: string; risk?: string; @@ -22,36 +24,12 @@ export interface Profile { created?: string; } -export interface Question { - question?: string; - answer?: string | number[]; -} - -export enum FormControlType { - SELECT = 'select', - TEXTAREA = 'text-long', - EMAIL_MULTIPLE = 'email-multiple', - SELECT_MULTIPLE = 'select-multiple', - TEXT = 'text', -} - -export interface Validation { - required?: boolean; - max?: string; -} - -export interface ProfileFormat { - question: string; - type: FormControlType; - description?: string; - options?: string[]; - default?: string; - validation?: Validation; -} +export type ProfileFormat = QuestionFormat; export interface Question { question?: string; answer?: string | number[]; + default?: string | number[]; } export enum ProfileRisk { @@ -62,9 +40,15 @@ export enum ProfileRisk { export enum ProfileStatus { VALID = 'Valid', DRAFT = 'Draft', + EXPIRED = 'Expired', } export interface RiskResultClassName { red: boolean; cyan: boolean; } + +export enum ProfileAction { + Copy = 'Copy', + Delete = 'Delete', +} diff --git a/modules/ui/src/app/pages/testrun/testrun-routing.module.ts b/modules/ui/src/app/model/program-type.ts similarity index 63% rename from modules/ui/src/app/pages/testrun/testrun-routing.module.ts rename to modules/ui/src/app/model/program-type.ts index 903ce0bed..6c6d8b9f5 100644 --- a/modules/ui/src/app/pages/testrun/testrun-routing.module.ts +++ b/modules/ui/src/app/model/program-type.ts @@ -13,14 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { TestrunComponent } from './testrun.component'; - -const routes: Routes = [{ path: '', component: TestrunComponent }]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class TestrunRoutingModule {} +export enum ProgramType { + Pilot = 'pilot', + Qualification = 'qualification', +} diff --git a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.scss b/modules/ui/src/app/model/question.ts similarity index 56% rename from modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.scss rename to modules/ui/src/app/model/question.ts index 85ddce69d..284fcdd3d 100644 --- a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.scss +++ b/modules/ui/src/app/model/question.ts @@ -1,4 +1,4 @@ -/** +/* * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,32 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../theming/colors'; - -:host { - display: grid; - overflow: hidden; - width: 570px; - box-sizing: border-box; - padding: 24px 16px 8px 24px; - gap: 10px; +export interface Validation { + required: boolean | undefined; + max?: string; } -.modal-title { - color: $grey-900; - font-size: 18px; - line-height: 24px; +export enum FormControlType { + SELECT = 'select', + TEXTAREA = 'text-long', + EMAIL_MULTIPLE = 'email-multiple', + SELECT_MULTIPLE = 'select-multiple', + TEXT = 'text', } -.modal-content { - font-family: Roboto, sans-serif; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.2px; - color: $grey-800; +export interface QuestionFormat { + question: string; + type: FormControlType; + description?: string; + options?: OptionType[]; + default?: string; + validation?: Validation; } -.modal-actions { - padding: 0; - min-height: 30px; -} +export type OptionType = string | object; diff --git a/modules/ui/src/app/model/routes.ts b/modules/ui/src/app/model/routes.ts index 05ff91ed9..3028417a4 100644 --- a/modules/ui/src/app/model/routes.ts +++ b/modules/ui/src/app/model/routes.ts @@ -16,7 +16,10 @@ export enum Routes { Devices = '/devices', + Settings = '/settings', Testing = '/testing', Reports = '/reports', RiskAssessment = '/risk-assessment', + Certificates = 'certificates', + General = 'general', } diff --git a/modules/ui/src/app/model/setting.ts b/modules/ui/src/app/model/setting.ts index 5e71052f3..02c3ec0ce 100644 --- a/modules/ui/src/app/model/setting.ts +++ b/modules/ui/src/app/model/setting.ts @@ -20,6 +20,7 @@ export interface SystemConfig { } | null; log_level?: string; monitor_period?: number; + single_intf?: boolean; } export interface InterfacesValidation { @@ -48,3 +49,8 @@ export enum FormKey { export type SystemInterfaces = { [key: string]: string; }; + +export type Adapters = { + adapters_added?: SystemInterfaces; + adapters_removed?: SystemInterfaces; +}; diff --git a/modules/ui/src/app/model/testrun-status.ts b/modules/ui/src/app/model/testrun-status.ts index 2ac908185..a990a2a45 100644 --- a/modules/ui/src/app/model/testrun-status.ts +++ b/modules/ui/src/app/model/testrun-status.ts @@ -16,18 +16,24 @@ import { Device } from './device'; export interface TestrunStatus { - mac_addr: string; - status: string; + mac_addr: string | null; + status: StatusOfTestrun; + result?: ResultOfTestrun; + description?: string; device: IDevice; started: string | null; finished: string | null; tests?: TestsResponse; - report?: string; + report: string; + export: string; + tags: string[] | null; } export interface HistoryTestrun extends TestrunStatus { deviceFirmware: string; deviceInfo: string; + testResult: string; + program: string; duration: string; } @@ -47,6 +53,18 @@ export interface IResult { description: string; result: string; recommendations?: string[]; + required_result: RequiredResult; +} + +export enum RequiredResult { + Informational = 'Informational', + Required = 'Required', + RequiredIfApplicable = 'Required if Applicable', +} + +export enum ResultOfTestrun { + Compliant = 'Compliant', // used for Completed + NonCompliant = 'Non-Compliant', // used for Completed } export enum StatusOfTestrun { @@ -55,14 +73,17 @@ export enum StatusOfTestrun { Cancelled = 'Cancelled', Cancelling = 'Cancelling', Failed = 'Failed', - Compliant = 'Compliant', // used for Completed CompliantLimited = 'Compliant (Limited)', CompliantHigh = 'Compliant (High)', - NonCompliant = 'Non-Compliant', // used for Completed - SmartReady = 'Smart Ready', // used for Completed + SmartReady = 'Smart Ready', Idle = 'Idle', Monitoring = 'Monitoring', + Starting = 'Starting', Error = 'Error', + Validating = 'Validating Network', + Complete = 'Complete', // device qualification + Proceed = 'Proceed', // pilot assessment + DoNotProceed = 'Do Not Proceed', // pilot assessment } export enum StatusOfTestResult { @@ -75,16 +96,34 @@ export enum StatusOfTestResult { NotStarted = 'Not Started', InProgress = 'In Progress', Error = 'Error', // test failed to run - Info = 'Informational', // nice to know information, not necessarily compliant/non-compliant + Info = 'Informational', // nice to know information, not necessarily compliant/non-compliant, + Skipped = 'Skipped', + Disabled = 'Disabled', } export interface StatusResultClassName { green: boolean; red: boolean; blue: boolean; + cyan: boolean; grey: boolean; } +export const IDLE_STATUS = { + status: StatusOfTestrun.Idle, + device: {} as IDevice, + started: null, + finished: null, + report: '', + export: '', + mac_addr: '', + tests: { + total: 0, + results: [], + }, + tags: [], +} as TestrunStatus; + export type TestrunStatusKey = keyof typeof StatusOfTestrun; export type TestrunStatusValue = (typeof StatusOfTestrun)[TestrunStatusKey]; export type TestResultKey = keyof typeof StatusOfTestResult; diff --git a/modules/ui/src/app/model/tip-config.ts b/modules/ui/src/app/model/tip-config.ts new file mode 100644 index 000000000..521b5c143 --- /dev/null +++ b/modules/ui/src/app/model/tip-config.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface TipConfig { + title: string; + content: string; + action: string; + arrowPosition: 'left' | 'right' | 'top' | 'bottom'; + position: 'left' | 'right' | 'top' | 'bottom'; // Position related to the target +} + +export const HelpTips = { + step1: { + title: 'Step 1:', + content: + 'To get started testing, please select your testing interfaces in system\n' + + 'settings.', + action: 'Go to Settings', + position: 'bottom', + arrowPosition: 'top', + } as TipConfig, + step2: { + title: 'Step 2:', + content: 'Create a device to start your first test attempt.', + action: 'Create Device', + position: 'right', + arrowPosition: 'left', + } as TipConfig, + step3: { + title: 'Step 3:', + content: 'You can now start your first test attempt your new device.', + action: 'Start Testrun', + position: 'right', + arrowPosition: 'left', + } as TipConfig, + step4: { + title: 'Risk Assessment:', + content: + 'Whilst testing is in progress, create a risk profile for the device.', + action: 'Create risk profile', + position: 'right', + arrowPosition: 'left', + } as TipConfig, +}; diff --git a/modules/ui/src/app/model/topic.ts b/modules/ui/src/app/model/topic.ts new file mode 100644 index 000000000..d330dbb82 --- /dev/null +++ b/modules/ui/src/app/model/topic.ts @@ -0,0 +1,9 @@ +export enum Topic { + NetworkAdapters = 'events/adapter', + InternetConnection = 'events/internet', + Status = 'status', +} + +export interface InternetConnection { + connection: boolean | null; +} diff --git a/modules/ui/src/app/model/version.ts b/modules/ui/src/app/model/version.ts index 21ec6ee38..5eb946e42 100644 --- a/modules/ui/src/app/model/version.ts +++ b/modules/ui/src/app/model/version.ts @@ -23,5 +23,4 @@ export interface Version { export interface ConsentDialogResult { grant: boolean; - isNavigateToRiskAssessment?: boolean; } diff --git a/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.html b/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.html deleted file mode 100644 index 43da6e299..000000000 --- a/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.html +++ /dev/null @@ -1,39 +0,0 @@ -
- - workspace_premium - - - -
-

{{ certificate.name }}

-

- {{ certificate.organisation }} -

-

- {{ certificate.expires | date: 'dd MMM yyyy' }} -

- -
- -
diff --git a/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.scss b/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.scss deleted file mode 100644 index 394ddec83..000000000 --- a/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.scss +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import 'src/theming/colors'; -@import 'src/theming/variables'; - -:host { - ::ng-deep .mat-mdc-progress-bar { - --mdc-linear-progress-active-indicator-color: #1967d2; - } -} - -:host:first-child .certificate-item-container { - border-top: 1px solid $lighter-grey; -} - -.certificate-item-container { - display: grid; - grid-template-columns: 24px minmax(200px, 1fr) 24px; - gap: 16px; - box-sizing: border-box; - padding: 12px 0; - border-bottom: 1px solid $lighter-grey; -} - -.certificate-item-icon { - color: $grey-700; -} - -.certificate-item-delete { - padding: 0; - height: 24px; - width: 24px; - border-radius: 4px; - color: $grey-700; - display: flex; - align-items: flex-start; - justify-content: center; - & ::ng-deep .mat-mdc-button-persistent-ripple { - border-radius: 4px; - } - &:disabled { - pointer-events: none; - opacity: 0.6; - } -} - -.certificate-item-information { - overflow: hidden; - p { - font-family: $font-secondary, sans-serif; - margin: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .certificate-item-name { - font-size: 16px; - color: $grey-800; - min-height: 24px; - } - .certificate-item-organisation, - .certificate-item-expires { - font-size: 14px; - color: $grey-700; - min-height: 20px; - } -} - -.certificate-expired { - .certificate-item-icon { - color: $red-700; - } - .certificate-item-name { - color: $red-800; - } - - .certificate-item-organisation, - .certificate-item-expires { - color: $red-700; - } -} diff --git a/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.ts b/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.ts deleted file mode 100644 index 0eea2f7ec..000000000 --- a/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { Certificate, CertificateStatus } from '../../../model/certificate'; -import { MatIcon } from '@angular/material/icon'; -import { CommonModule } from '@angular/common'; -import { MatButtonModule } from '@angular/material/button'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { provideAnimations } from '@angular/platform-browser/animations'; -import { MatError } from '@angular/material/form-field'; - -@Component({ - selector: 'app-certificate-item', - standalone: true, - imports: [ - MatIcon, - MatButtonModule, - MatProgressBarModule, - CommonModule, - MatError, - ], - providers: [provideAnimations()], - templateUrl: './certificate-item.component.html', - styleUrl: './certificate-item.component.scss', -}) -export class CertificateItemComponent { - @Input() certificate!: Certificate; - @Output() deleteButtonClicked = new EventEmitter(); - - CertificateStatus = CertificateStatus; -} diff --git a/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.scss b/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.scss deleted file mode 100644 index e48f64ceb..000000000 --- a/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -.browse-files-button { - margin: 18px 16px; - padding: 8px 24px; - font-size: 14px; - font-weight: 500; - line-height: 20px; - letter-spacing: 0.25px; - height: auto; - min-height: 36px; -} - -#default-file-input { - display: none; -} diff --git a/modules/ui/src/app/pages/certificates/certificate.validator.ts b/modules/ui/src/app/pages/certificates/certificate.validator.ts index 9143824e8..72e197af1 100644 --- a/modules/ui/src/app/pages/certificates/certificate.validator.ts +++ b/modules/ui/src/app/pages/certificates/certificate.validator.ts @@ -9,7 +9,6 @@ export const getValidationErrors = (file: File): string[] => { errors.push(validateExtension(file.name)); errors.push(validateFileNameLength(file.name)); errors.push(validateSize(file.size)); - // @ts-expect-error null values are filtered return errors.filter(error => error !== null); }; const validateFileName = (name: string): string | null => { diff --git a/modules/ui/src/app/pages/certificates/certificates.component.html b/modules/ui/src/app/pages/certificates/certificates.component.html index d75f115b4..e13bbffcb 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.html +++ b/modules/ui/src/app/pages/certificates/certificates.component.html @@ -13,40 +13,16 @@ See the License for the specific language governing permissions and limitations under the License. --> - -
-

Certificates

- -
-
- -
- -
- -
-
+
+ +
+
+ + +
diff --git a/modules/ui/src/app/pages/certificates/certificates.component.scss b/modules/ui/src/app/pages/certificates/certificates.component.scss index 5ee4e9847..ee891a214 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.scss +++ b/modules/ui/src/app/pages/certificates/certificates.component.scss @@ -14,77 +14,21 @@ * limitations under the License. */ @use '@angular/material' as mat; -@import '../../../theming/colors'; -@import '../../../theming/variables'; +@use 'colors'; +@use 'variables'; :host { display: flex; flex-direction: column; - height: 100%; flex: 1 0 auto; } -.certificates-drawer-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 12px 16px 24px; - - &-title { - margin: 0; - font-size: 22px; - font-style: normal; - font-weight: 400; - line-height: 28px; - color: $dark-grey; - } - - &-button { - min-width: 24px; - width: 24px; - height: 24px; - margin: 4px; - padding: 8px !important; - box-sizing: content-box; - line-height: normal !important; - - .close-button-icon { - width: 24px; - height: 24px; - margin: 0; - } - - ::ng-deep * { - line-height: inherit !important; - } - } -} - -.certificates-drawer-content { - overflow: hidden; - flex: 1; - display: grid; - grid-template-rows: auto 1fr auto; -} - .content-certificates { - padding: 0 16px; - border-bottom: 1px solid $lighter-grey; - overflow-y: scroll; + margin: 2px 18px 0; + height: max-content; + padding: 0 6px 6px; } -.certificates-drawer-footer { - padding: 16px 24px 8px 16px; - margin-top: auto; - display: flex; - flex-shrink: 0; - justify-content: flex-end; - - .close-button { - padding: 0 24px; - font-size: 14px; - font-weight: 500; - line-height: 20px; - letter-spacing: 0.25px; - } +.certificates-button-container { + margin: 24px; } diff --git a/modules/ui/src/app/pages/certificates/certificates.component.spec.ts b/modules/ui/src/app/pages/certificates/certificates.component.spec.ts index c8f0e91b6..139c2605d 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.spec.ts +++ b/modules/ui/src/app/pages/certificates/certificates.component.spec.ts @@ -75,34 +75,6 @@ describe('CertificatesComponent', () => { }); describe('DOM tests', () => { - it('should emit closeSettingEvent when header button clicked', () => { - const headerCloseButton = fixture.nativeElement.querySelector( - '.certificates-drawer-header-button' - ) as HTMLButtonElement; - spyOn(component.closeCertificatedEvent, 'emit'); - - headerCloseButton.click(); - - expect(mockLiveAnnouncer.announce).toHaveBeenCalledWith( - 'The certificates panel is closed.' - ); - expect(component.closeCertificatedEvent.emit).toHaveBeenCalled(); - }); - - it('should emit closeSettingEvent when close button clicked', () => { - const headerCloseButton = fixture.nativeElement.querySelector( - '.close-button' - ) as HTMLButtonElement; - spyOn(component.closeCertificatedEvent, 'emit'); - - headerCloseButton.click(); - - expect(mockLiveAnnouncer.announce).toHaveBeenCalledWith( - 'The certificates panel is closed.' - ); - expect(component.closeCertificatedEvent.emit).toHaveBeenCalled(); - }); - it('should have upload file button', () => { const uploadCertificatesButton = fixture.nativeElement.querySelector( '.browse-files-button' @@ -113,9 +85,8 @@ describe('CertificatesComponent', () => { describe('with certificates', () => { it('should have certificates list', () => { - const certificateList = fixture.nativeElement.querySelectorAll( - 'app-certificate-item' - ); + const certificateList = + fixture.nativeElement.querySelectorAll('.cdk-row'); expect(certificateList.length).toEqual(2); }); @@ -142,7 +113,7 @@ describe('CertificatesComponent', () => { autoFocus: true, hasBackdrop: true, disableClose: true, - panelClass: 'simple-dialog', + panelClass: ['simple-dialog', 'delete-certificate'], }); openSpy.calls.reset(); @@ -151,12 +122,10 @@ describe('CertificatesComponent', () => { describe('#focusNextButton', () => { it('should focus next active element if exist', fakeAsync(() => { - const row = window.document.querySelector( - 'app-certificate-item' - ) as HTMLElement; + const row = window.document.querySelector('.cdk-row') as HTMLElement; row.classList.add('certificate-selected'); const nextButton = window.document.querySelector( - '.certificate-selected + app-certificate-item .certificate-item-delete' + '.certificate-selected + .cdk-row .certificate-item-delete' ) as HTMLButtonElement; const buttonFocusSpy = spyOn(nextButton, 'focus'); @@ -166,9 +135,9 @@ describe('CertificatesComponent', () => { flush(); })); - it('should focus navigation button if next active element does not exist', fakeAsync(() => { + it('should focus upload button if next active element does not exist', fakeAsync(() => { const nextButton = window.document.querySelector( - '.certificates-drawer-content .close-button' + '.browse-files-button' ) as HTMLButtonElement; const buttonFocusSpy = spyOn(nextButton, 'focus'); diff --git a/modules/ui/src/app/pages/certificates/certificates.component.ts b/modules/ui/src/app/pages/certificates/certificates.component.ts index bf6cf19fa..b3c6065d5 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.ts +++ b/modules/ui/src/app/pages/certificates/certificates.component.ts @@ -13,57 +13,47 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, EventEmitter, OnDestroy, Output } from '@angular/core'; -import { MatIcon } from '@angular/material/icon'; -import { CertificateItemComponent } from './certificate-item/certificate-item.component'; +import { + Component, + EventEmitter, + OnDestroy, + Output, + inject, +} from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; -import { CdkTrapFocus, LiveAnnouncer } from '@angular/cdk/a11y'; -import { CertificateUploadButtonComponent } from './certificate-upload-button/certificate-upload-button.component'; import { CertificatesStore } from './certificates.store'; import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; import { Subject, takeUntil } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; +import { CertificatesTableComponent } from './components/certificates-table/certificates-table.component'; +import { CertificateUploadButtonComponent } from './components/certificate-upload-button/certificate-upload-button.component'; @Component({ selector: 'app-certificates', - standalone: true, imports: [ - MatIcon, - CertificateItemComponent, MatButtonModule, CertificateUploadButtonComponent, CommonModule, + CertificatesTableComponent, ], providers: [CertificatesStore, DatePipe], - hostDirectives: [CdkTrapFocus], templateUrl: './certificates.component.html', styleUrl: './certificates.component.scss', }) export class CertificatesComponent implements OnDestroy { - viewModel$ = this.store.viewModel$; + store = inject(CertificatesStore); + dialog = inject(MatDialog); + @Output() closeCertificatedEvent = new EventEmitter(); private destroy$: Subject = new Subject(); - constructor( - private liveAnnouncer: LiveAnnouncer, - private store: CertificatesStore, - public dialog: MatDialog - ) { - this.store.getCertificates(); - } - ngOnDestroy() { this.destroy$.next(true); this.destroy$.unsubscribe(); } - closeCertificates() { - this.liveAnnouncer.announce('The certificates panel is closed.'); - this.closeCertificatedEvent.emit(); - } - uploadFile(file: File) { this.store.uploadCertificate(file); } @@ -80,7 +70,7 @@ export class CertificatesComponent implements OnDestroy { autoFocus: true, hasBackdrop: true, disableClose: true, - panelClass: 'simple-dialog', + panelClass: ['simple-dialog', 'delete-certificate'], }); dialogRef @@ -97,16 +87,16 @@ export class CertificatesComponent implements OnDestroy { focusNextButton() { // Try to focus next interactive element, if exists const next = window.document.querySelector( - '.certificate-selected + app-certificate-item .certificate-item-delete' + '.certificate-selected + .cdk-row .certificate-item-delete' ) as HTMLButtonElement; if (next) { next.focus(); } else { - // If next interactive element doest not exist, close button will be focused - const menuButton = window.document.querySelector( - '.certificates-drawer-content .close-button' + // If next interactive element doest not exist, upload button will be focused + const uploadButton = window.document.querySelector( + '.browse-files-button' ) as HTMLButtonElement; - menuButton?.focus(); + uploadButton?.focus(); } } } diff --git a/modules/ui/src/app/pages/certificates/certificates.store.spec.ts b/modules/ui/src/app/pages/certificates/certificates.store.spec.ts index 06e3accf6..66d01fde6 100644 --- a/modules/ui/src/app/pages/certificates/certificates.store.spec.ts +++ b/modules/ui/src/app/pages/certificates/certificates.store.spec.ts @@ -1,208 +1,98 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ import { TestBed } from '@angular/core/testing'; -import { of, skip, take } from 'rxjs'; -import { provideMockStore } from '@ngrx/store/testing'; -import { TestRunService } from '../../services/test-run.service'; -import SpyObj = jasmine.SpyObj; -import { - certificate, - certificate2, - certificate_uploading, - FILE, - INVALID_FILE, -} from '../../mocks/certificate.mock'; import { CertificatesStore } from './certificates.store'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { DatePipe } from '@angular/common'; +import { TestRunService } from '../../services/test-run.service'; import { NotificationService } from '../../services/notification.service'; +import { DatePipe } from '@angular/common'; +import { of, throwError } from 'rxjs'; +import { Certificate } from '../../model/certificate'; describe('CertificatesStore', () => { - let certificateStore: CertificatesStore; - let mockService: SpyObj; - const notificationServiceMock: jasmine.SpyObj = - jasmine.createSpyObj(['notify']); - + // @ts-expect-error certificatesStore is a ReturnType of CertificatesStore + let certificatesStore: ReturnType; + const mockCertificates: Certificate[] = [ + { name: 'Cert1', uploading: false }, + { name: 'Cert2', uploading: false }, + ]; + const testRunServiceMock = jasmine.createSpyObj('TestRunService', [ + 'fetchCertificates', + 'deleteCertificate', + 'uploadCertificate', + ]); + const notificationServiceMock = jasmine.createSpyObj('NotificationService', [ + 'notify', + ]); beforeEach(() => { - mockService = jasmine.createSpyObj([ - 'fetchCertificates', - 'uploadCertificate', - 'deleteCertificate', - ]); - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule], providers: [ - CertificatesStore, - provideMockStore({}), - { provide: TestRunService, useValue: mockService }, + { provide: TestRunService, useValue: testRunServiceMock }, { provide: NotificationService, useValue: notificationServiceMock }, DatePipe, + CertificatesStore, ], }); + testRunServiceMock.fetchCertificates.and.returnValue(of(mockCertificates)); - certificateStore = TestBed.inject(CertificatesStore); + certificatesStore = TestBed.inject(CertificatesStore); }); - it('should be created', () => { - expect(certificateStore).toBeTruthy(); - }); - - describe('updaters', () => { - it('should update certificates', (done: DoneFn) => { - certificateStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.certificates).toEqual([certificate]); - done(); - }); + it('should initialize with certificates fetched from the service', () => { + certificatesStore.getCertificates(); - certificateStore.updateCertificates([certificate]); - }); + expect(testRunServiceMock.fetchCertificates).toHaveBeenCalled(); + expect(certificatesStore.certificates()).toEqual(mockCertificates); + }); - it('should update selectedCertificate', (done: DoneFn) => { - const certificate = 'test'; - certificateStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.selectedCertificate).toEqual(certificate); - done(); - }); + it('should handle errors when fetching certificates', () => { + testRunServiceMock.fetchCertificates.and.returnValue(throwError('Error')); - certificateStore.selectCertificate(certificate); - }); - }); + certificatesStore.getCertificates(); - describe('selectors', () => { - it('should select state', done => { - certificateStore.viewModel$.pipe(take(1)).subscribe(store => { - expect(store).toEqual({ - certificates: [], - selectedCertificate: '', - }); - done(); - }); - }); + expect(certificatesStore.certificates()).toEqual([]); }); - describe('effects', () => { - describe('fetchCertificates', () => { - const certificates = [certificate]; + it('should delete a certificate and update the store', () => { + testRunServiceMock.deleteCertificate.and.returnValue(of(true)); - beforeEach(() => { - mockService.fetchCertificates.and.returnValue(of(certificates)); - }); + certificatesStore.deleteCertificate('Cert1'); - it('should update certificates', done => { - certificateStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.certificates).toEqual(certificates); - done(); - }); + expect(testRunServiceMock.deleteCertificate).toHaveBeenCalledWith('Cert1'); + expect(certificatesStore.certificates()).toEqual([ + { name: 'Cert2', uploading: false }, + ]); + }); - certificateStore.getCertificates(); - }); - }); + it('should handle errors when deleting a certificate', () => { + testRunServiceMock.deleteCertificate.and.returnValue(throwError('Error')); - describe('uploadCertificate', () => { - beforeEach(() => { - mockService.uploadCertificate.and.returnValue(of(true)); - mockService.fetchCertificates.and.returnValue(of([certificate])); - }); - - describe('with valid certificate file', () => { - it('should update certificates', done => { - const uploadingCertificate = certificate_uploading; - - certificateStore.viewModel$ - .pipe(skip(1), take(1)) - .subscribe(store => { - expect(store.certificates).toContain(uploadingCertificate); - }); - - certificateStore.viewModel$ - .pipe(skip(2), take(1)) - .subscribe(store => { - expect(store.certificates).toEqual([certificate]); - done(); - }); - - certificateStore.uploadCertificate(FILE); - }); - - it('should notify', () => { - const container = document.createElement('DIV'); - container.classList.add('certificates-drawer-content'); - document.querySelector('body')?.appendChild(container); - certificateStore.uploadCertificate(FILE); - - expect(notificationServiceMock.notify).toHaveBeenCalledWith( - 'Certificate successfully added.\niot.bms.google.com by Google, Inc. valid until 01 Sep 2024', - 0, - 'certificate-notification', - 10000, - container - ); - }); - }); - - describe('with invalid certificate file', () => { - it('should notify about errors', () => { - const container = document.createElement('DIV'); - container.classList.add('certificates-drawer-content'); - document.querySelector('body')?.appendChild(container); - certificateStore.uploadCertificate(INVALID_FILE); - - expect(notificationServiceMock.notify).toHaveBeenCalledWith( - 'File "some very long strange n..." is not added.\nThe file name should be alphanumeric, symbols -_. are allowed.\nFile extension must be .cert, .crt, .pem, .cer.\nMax name length is 24 characters.\nFile size should be a max of 4KB', - 0, - 'certificate-notification', - 24000, - container - ); - }); - }); - - it('should not upload certificates if error happens', done => { - mockService.uploadCertificate.and.returnValue(of(false)); - mockService.fetchCertificates.and.returnValue(of([])); - - const uploadingCertificate = certificate_uploading; - - certificateStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.certificates).toContain(uploadingCertificate); - }); - - certificateStore.viewModel$.pipe(skip(2), take(1)).subscribe(store => { - expect(store.certificates).not.toContain(certificate); - done(); - }); - - certificateStore.uploadCertificate(FILE); - }); - }); + certificatesStore.deleteCertificate('Cert1'); + expect(certificatesStore.certificates()).toEqual(mockCertificates); + }); - describe('deleteCertificate', () => { - it('should update store', done => { - mockService.deleteCertificate.and.returnValue(of(true)); + it('should upload a certificate and update the store', () => { + const mockFile = new File(['content'], 'Cert1.crt'); + const uploadedCertificates: Certificate[] = [ + { name: 'Cert1', uploading: false }, + ]; + testRunServiceMock.uploadCertificate.and.returnValue(of(true)); + testRunServiceMock.fetchCertificates.and.returnValue( + of(uploadedCertificates) + ); - certificateStore.updateCertificates([certificate, certificate2]); + certificatesStore.uploadCertificate(mockFile); - certificateStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.certificates).toEqual([certificate2]); - done(); - }); + expect(testRunServiceMock.uploadCertificate).toHaveBeenCalledWith(mockFile); + expect(certificatesStore.certificates()).toEqual(uploadedCertificates); + }); - certificateStore.deleteCertificate(certificate.name); - }); + it('should notify and revert on upload error', () => { + const mockFile = new File(['content'], 'Cert1.pdf', { + type: 'application/pdf', }); + testRunServiceMock.uploadCertificate.and.returnValue(throwError('Error')); + + certificatesStore.uploadCertificate(mockFile); + + expect(notificationServiceMock.notify).toHaveBeenCalled(); + expect(certificatesStore.certificates()).toEqual(mockCertificates); }); }); diff --git a/modules/ui/src/app/pages/certificates/certificates.store.ts b/modules/ui/src/app/pages/certificates/certificates.store.ts index 21f96eed0..9d14a014b 100644 --- a/modules/ui/src/app/pages/certificates/certificates.store.ts +++ b/modules/ui/src/app/pages/certificates/certificates.store.ts @@ -14,156 +14,152 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; -import { ComponentStore } from '@ngrx/component-store'; -import { switchMap, tap, withLatestFrom } from 'rxjs/operators'; -import { catchError, EMPTY, exhaustMap, of, throwError } from 'rxjs'; +import { computed, inject } from '@angular/core'; +import { signalStore } from '@ngrx/signals'; +import { switchMap, tap } from 'rxjs/operators'; +import { catchError, EMPTY, exhaustMap, throwError } from 'rxjs'; import { Certificate } from '../../model/certificate'; import { TestRunService } from '../../services/test-run.service'; import { NotificationService } from '../../services/notification.service'; import { DatePipe } from '@angular/common'; import { FILE_NAME_LENGTH, getValidationErrors } from './certificate.validator'; - -export interface AppComponentState { - certificates: Certificate[]; - selectedCertificate: string; -} +import { + withState, + withHooks, + withMethods, + patchState, + withComputed, +} from '@ngrx/signals'; +import { tapResponse } from '@ngrx/operators'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { MatTableDataSource } from '@angular/material/table'; const SYMBOLS_PER_SECOND = 9.5; -@Injectable() -export class CertificatesStore extends ComponentStore { - private certificates$ = this.select(state => state.certificates); - private selectedCertificate$ = this.select( - state => state.selectedCertificate - ); - - viewModel$ = this.select({ - certificates: this.certificates$, - selectedCertificate: this.selectedCertificate$, - }); - - updateCertificates = this.updater((state, certificates: Certificate[]) => ({ - ...state, - certificates, - })); - - selectCertificate = this.updater((state, selectedCertificate: string) => ({ - ...state, - selectedCertificate, - })); - getCertificates = this.effect(trigger$ => { - return trigger$.pipe( - exhaustMap(() => { - return this.testRunService.fetchCertificates().pipe( - tap((certificates: Certificate[]) => { - this.updateCertificates(certificates); - }) +export const CertificatesStore = signalStore( + withState({ + certificates: [] as Certificate[], + selectedCertificate: '', + displayedColumns: ['name', 'organisation', 'expires', 'status', 'actions'], + dataLoaded: false, + }), + withComputed(({ certificates }) => ({ + dataSource: computed(() => new MatTableDataSource(certificates())), + })), + withMethods( + ( + store, + testRunService = inject(TestRunService), + notificationService = inject(NotificationService), + datePipe = inject(DatePipe) + ) => { + function removeCertificate(name: string) { + patchState(store, { + certificates: store + .certificates() + .filter(certificate => certificate.name !== name), + }); + } + function addCertificate(name: string, certificates: Certificate[]) { + const certificate = { name, uploading: true } as Certificate; + patchState(store, { + certificates: [certificate, ...certificates], + }); + } + function notify(message: string) { + notificationService.notify( + message, + 0, + 'certificate-notification', + Math.ceil(message.length / SYMBOLS_PER_SECOND) * 1000, + window.document.querySelector('.certificates-drawer-content') ); - }) - ); - }); - - uploadCertificate = this.effect(trigger$ => { - return trigger$.pipe( - withLatestFrom(this.certificates$), - switchMap(res => { - const [file] = res; - const errors = getValidationErrors(file); - if (errors.length > 0) { - errors.unshift( - `File "${this.getShortCertificateName(file.name)}" is not added.` - ); - this.notify(errors.join('\n')); - return EMPTY; - } - return of(res); - }), - tap(res => { - const [file, certificates] = res; - this.addCertificate(file.name, certificates); - }), - exhaustMap(([file, certificates]) => { - return this.testRunService.uploadCertificate(file).pipe( - exhaustMap(uploaded => { - if (uploaded) { - return this.testRunService.fetchCertificates(); + } + function getShortCertificateName(name: string) { + return name.length <= FILE_NAME_LENGTH + ? name + : `${name.substring(0, FILE_NAME_LENGTH)}...`; + } + return { + getShortCertificateName, + selectCertificate: (certificate: string) => { + patchState(store, { + selectedCertificate: certificate, + }); + }, + getCertificates: rxMethod( + switchMap(() => + testRunService.fetchCertificates().pipe( + tapResponse({ + next: certificates => + patchState(store, { certificates, dataLoaded: true }), + error: () => patchState(store, { certificates: [] }), + }) + ) + ) + ), + deleteCertificate: rxMethod( + switchMap((certificate: string) => + testRunService.deleteCertificate(certificate).pipe( + tapResponse({ + next: remove => { + if (remove) { + removeCertificate(certificate); + } + }, + error: () => + patchState(store, { certificates: store.certificates() }), + }) + ) + ) + ), + uploadCertificate: rxMethod( + switchMap((file: File) => { + const errors = getValidationErrors(file); + if (errors.length > 0) { + errors.unshift( + `File "${getShortCertificateName(file.name)}" is not added.` + ); + notify(errors.join('\n')); + return EMPTY; } - return throwError('Failed to upload certificate'); - }), - tap(newCertificates => { - const uploadedCertificate = newCertificates.filter( - certificate => - !certificates.some(cert => cert.name === certificate.name) - )[0]; - this.updateCertificates(newCertificates); - this.notify( - `Certificate successfully added.\n${uploadedCertificate.name} by ${uploadedCertificate.organisation} valid until ${this.datePipe.transform(uploadedCertificate.expires, 'dd MMM yyyy')}` + addCertificate(file.name, store.certificates()); + return testRunService.uploadCertificate(file).pipe( + exhaustMap(uploaded => { + if (uploaded) { + return testRunService.fetchCertificates(); + } + return throwError('Failed to upload certificate'); + }), + tap(newCertificates => { + const uploadedCertificate = newCertificates.filter( + certificate => + !store + .certificates() + .some(cert => cert.name === certificate.name) + )[0]; + patchState(store, { certificates: newCertificates }); + // @ts-expect-error data layer is not null + window.dataLayer.push({ + event: 'successful_saving_certificate', + }); + notify( + `Certificate successfully added.\n${uploadedCertificate.name} by ${uploadedCertificate.organisation} valid until ${datePipe.transform(uploadedCertificate.expires, 'dd MMM yyyy')}` + ); + }), + catchError(() => { + removeCertificate(file.name); + return EMPTY; + }) ); - }), - catchError(() => { - this.removeCertificate(file.name, certificates); - return EMPTY; - }) - ); - }) - ); - }); - - addCertificate(name: string, certificates: Certificate[]) { - const certificate = { name, uploading: true } as Certificate; - this.updateCertificates([certificate, ...certificates]); - } - - deleteCertificate = this.effect(trigger$ => { - return trigger$.pipe( - withLatestFrom(this.certificates$), - exhaustMap(([certificate, current]) => { - return this.testRunService.deleteCertificate(certificate).pipe( - tap(remove => { - if (remove) { - this.removeCertificate(certificate, current); - } - }), - catchError(() => { - return EMPTY; }) - ); - }) - ); - }); - - getShortCertificateName(name: string) { - return name.length <= FILE_NAME_LENGTH - ? name - : `${name.substring(0, FILE_NAME_LENGTH)}...`; - } - - private notify(message: string) { - this.notificationService.notify( - message, - 0, - 'certificate-notification', - Math.ceil(message.length / SYMBOLS_PER_SECOND) * 1000, - window.document.querySelector('.certificates-drawer-content') - ); - } - - private removeCertificate(name: string, current: Certificate[]) { - const certificates = current.filter( - certificate => certificate.name !== name - ); - this.updateCertificates(certificates); - } - - constructor( - private testRunService: TestRunService, - private notificationService: NotificationService, - private datePipe: DatePipe - ) { - super({ - certificates: [], - selectedCertificate: '', - }); - } -} + ), + }; + } + ), + withHooks({ + onInit({ getCertificates }) { + getCertificates(); + }, + }) +); diff --git a/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.html b/modules/ui/src/app/pages/certificates/components/certificate-upload-button/certificate-upload-button.component.html similarity index 86% rename from modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.html rename to modules/ui/src/app/pages/certificates/components/certificate-upload-button/certificate-upload-button.component.html index 0c1724e55..5b9cadeb0 100644 --- a/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.html +++ b/modules/ui/src/app/pages/certificates/components/certificate-upload-button/certificate-upload-button.component.html @@ -1,7 +1,6 @@ diff --git a/modules/ui/src/app/pages/certificates/components/certificate-upload-button/certificate-upload-button.component.scss b/modules/ui/src/app/pages/certificates/components/certificate-upload-button/certificate-upload-button.component.scss new file mode 100644 index 000000000..f89d7fd6b --- /dev/null +++ b/modules/ui/src/app/pages/certificates/components/certificate-upload-button/certificate-upload-button.component.scss @@ -0,0 +1,19 @@ +@use 'colors'; +@use 'variables'; + +.browse-files-button { + border-radius: 16px; + padding: 16px 24px; + font-family: variables.$font-text; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + height: auto; + background: colors.$secondary-container; + color: colors.$on-secondary-container; +} + +#default-file-input { + display: none; +} diff --git a/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.spec.ts b/modules/ui/src/app/pages/certificates/components/certificate-upload-button/certificate-upload-button.component.spec.ts similarity index 100% rename from modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.spec.ts rename to modules/ui/src/app/pages/certificates/components/certificate-upload-button/certificate-upload-button.component.spec.ts diff --git a/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.ts b/modules/ui/src/app/pages/certificates/components/certificate-upload-button/certificate-upload-button.component.ts similarity index 97% rename from modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.ts rename to modules/ui/src/app/pages/certificates/components/certificate-upload-button/certificate-upload-button.component.ts index 88cf522b4..dc481bad9 100644 --- a/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.ts +++ b/modules/ui/src/app/pages/certificates/components/certificate-upload-button/certificate-upload-button.component.ts @@ -3,7 +3,7 @@ import { MatButtonModule } from '@angular/material/button'; @Component({ selector: 'app-certificate-upload-button', - standalone: true, + imports: [MatButtonModule], templateUrl: './certificate-upload-button.component.html', styleUrl: './certificate-upload-button.component.scss', diff --git a/modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.html b/modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.html new file mode 100644 index 000000000..c2bb9bb5a --- /dev/null +++ b/modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.html @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Certificate Name + {{ data.name }} + + Organisation + + {{ data.organisation }} + + Expires + + {{ data.expires | date: 'dd MMM yyyy' }} + + Status + + + {{ data.status }} + + + +
+
+ + CA certificates must be uploaded to complete TLS testing + +
+
+ +
+
+
+ + + + + diff --git a/modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.scss b/modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.scss new file mode 100644 index 000000000..92dd71355 --- /dev/null +++ b/modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.scss @@ -0,0 +1,127 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@use 'variables'; +@use 'colors'; + +:host { + @include mat.table-overrides( + ( + background-color: colors.$surface, + header-headline-color: colors.$on-surface-variant, + row-item-label-text-color: colors.$on-surface-variant, + header-headline-font: variables.$font-text, + row-item-label-text-font: variables.$font-text, + footer-supporting-text-font: variables.$font-text, + row-item-outline-color: colors.$outline-variant, + ) + ); + + ::ng-deep .mdc-data-table__row:last-child .mat-mdc-cell { + border-bottom: 1px solid colors.$outline-variant; + } +} + +::ng-deep .delete-certificate app-simple-dialog { + width: 329px; +} + +.table-cell-actions { + text-align: right; +} + +.cell-result { + font-family: #{variables.$font-text}; + font-weight: 500; + margin: 0; + padding: 6px 12px; + border-radius: 8px; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.3px; + white-space: nowrap; + &.valid { + background: colors.$tertiary-container; + color: colors.$on-tertiary-container; + } + &.expired { + background: colors.$error-container; + color: colors.$on-error-container; + } +} + +.uploading { + background: rgba(196, 199, 197, 0.16); + font-style: italic; +} + +.results-content-empty-message { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +.results-content-empty-message-header { + font-weight: 400; + line-height: 28px; + font-size: 22px; + color: colors.$on-surface; +} + +.results-content-empty-message-main { + font-family: variables.$font-secondary; + font-weight: 400; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.1px; + color: colors.$on-surface-variant; +} + +.results-content-empty-message-img { + width: 293px; + height: 154px; + background-image: url(/assets/icons/desktop-new.svg); +} + +.certificates-content-empty { + min-height: 500px; +} + +.empty-data-cell { + position: relative; +} + +.callout-container { + display: flex; + position: absolute; + top: 0; + width: 100%; + + ::ng-deep .callout-container { + margin: 6px 0; + padding: 14px 16px; + } + + ::ng-deep .callout-context { + font-family: variables.$font-text; + letter-spacing: 0; + } +} + +.results-content-filter-empty { + margin-top: 60px; +} diff --git a/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.spec.ts b/modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.spec.ts similarity index 65% rename from modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.spec.ts rename to modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.spec.ts index 5840aa2bb..bcd275ed9 100644 --- a/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.spec.ts +++ b/modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.spec.ts @@ -1,25 +1,38 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CertificateItemComponent } from './certificate-item.component'; +import { CertificatesTableComponent } from './certificates-table.component'; +import { MatTableDataSource } from '@angular/material/table'; import { certificate, certificate_uploading, -} from '../../../mocks/certificate.mock'; +} from '../../../../mocks/certificate.mock'; -describe('CertificateItemComponent', () => { - let component: CertificateItemComponent; - let fixture: ComponentFixture; +describe('CertificatesTableComponent', () => { let compiled: HTMLElement; + let component: CertificatesTableComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CertificateItemComponent], + imports: [CertificatesTableComponent], }).compileComponents(); - fixture = TestBed.createComponent(CertificateItemComponent); - compiled = fixture.nativeElement as HTMLElement; + fixture = TestBed.createComponent(CertificatesTableComponent); component = fixture.componentInstance; - component.certificate = certificate; + fixture.componentRef.setInput( + 'dataSource', + new MatTableDataSource([certificate]) + ); + fixture.componentRef.setInput('selectedCertificate', ''); + fixture.componentRef.setInput('dataLoaded', true); + fixture.componentRef.setInput('displayedColumns', [ + 'name', + 'organisation', + 'expires', + 'status', + 'actions', + ]); + compiled = fixture.nativeElement as HTMLElement; fixture.detectChanges(); }); @@ -29,7 +42,7 @@ describe('CertificateItemComponent', () => { describe('DOM tests', () => { it('should have certificate name', () => { - const name = compiled.querySelector('.certificate-item-name'); + const name = compiled.querySelector('.cdk-row .mat-column-name'); expect(name?.textContent?.trim()).toEqual('iot.bms.google.com'); }); @@ -37,14 +50,14 @@ describe('CertificateItemComponent', () => { describe('uploaded certificate', () => { it('should have certificate organization', () => { const organization = compiled.querySelector( - '.certificate-item-organisation' + '.cdk-row .mat-column-organisation' ); expect(organization?.textContent?.trim()).toEqual('Google, Inc.'); }); it('should have certificate expire date', () => { - const date = compiled.querySelector('.certificate-item-expires'); + const date = compiled.querySelector('.cdk-row .mat-column-expires'); expect(date?.textContent?.trim()).toEqual('01 Sep 2024'); }); @@ -76,16 +89,13 @@ describe('CertificateItemComponent', () => { describe('uploading certificate', () => { beforeEach(() => { - component.certificate = certificate_uploading; + fixture.componentRef.setInput( + 'dataSource', + new MatTableDataSource([certificate_uploading]) + ); fixture.detectChanges(); }); - it('should have loader', () => { - const loader = compiled.querySelector('mat-progress-bar'); - - expect(loader).not.toBeNull(); - }); - it('should have disabled delete button', () => { const deleteButton = fixture.nativeElement.querySelector( '.certificate-item-delete' diff --git a/modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.ts b/modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.ts new file mode 100644 index 000000000..a03a49135 --- /dev/null +++ b/modules/ui/src/app/pages/certificates/components/certificates-table/certificates-table.component.ts @@ -0,0 +1,34 @@ +import { Component, input, output } from '@angular/core'; +import { Certificate } from '../../../../model/certificate'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule } from '@angular/common'; +import { CalloutType } from '../../../../model/callout-type'; +import { CalloutComponent } from '../../../../components/callout/callout.component'; +import { EmptyMessageComponent } from '../../../../components/empty-message/empty-message.component'; + +@Component({ + selector: 'app-certificates-table', + imports: [ + MatIconModule, + MatTableModule, + MatButtonModule, + CommonModule, + CalloutComponent, + EmptyMessageComponent, + ], + templateUrl: './certificates-table.component.html', + styleUrl: './certificates-table.component.scss', +}) +export class CertificatesTableComponent { + dataSource = input.required>(); + readonly CalloutType = CalloutType; + selectedCertificate = input(); + dataLoaded = input(false); + displayedColumns = input([]); + deleteButtonClicked = output(); + trackByName(index: number, item: Certificate) { + return item.name; + } +} diff --git a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.html b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.html deleted file mode 100644 index 5978f6d22..000000000 --- a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.html +++ /dev/null @@ -1,130 +0,0 @@ - -
- {{ data.title }} - - Device Manufacturer - - Please enter device manufacturer name - - Please, check. The manufacturer name must be a maximum of 28 - characters. Only letters, numbers, and accented letters are - permitted. - - - Device Manufacturer is required - - - - Device Model - - Please enter device name - - Please, check. The device model name must be a maximum of 28 - characters. Only letters, numbers, and accented letters are - permitted. - - - Device Model is required - - - - MAC address - - Please enter MAC address - - MAC address is required - - - Please, check. A MAC address consists of 12 hexadecimal digits (0 to 9, - a to f, or A to F). - - - This MAC address is already used for another device in the - repository. - - - - - - - {{ error$ | async }} - - - - - - - - - -
diff --git a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.scss b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.scss deleted file mode 100644 index 831f9907a..000000000 --- a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.scss +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import 'src/theming/colors'; - -$device-form-max-width: 580px; -$device-form-min-width: 285px; - -:host { - display: grid; - grid-template-rows: 1fr; - overflow: auto; - grid-template-columns: minmax(285px, $device-form-max-width); -} - -.device-form { - display: grid; - padding: 24px; - max-width: $device-form-max-width; - min-width: $device-form-min-width; - gap: 10px; - overflow: auto; -} - -.manufacturer-field, -.model-field { - &::ng-deep.mat-mdc-form-field-subscript-wrapper:has(mat-error) { - height: 40px; - } -} - -.device-form-title { - color: $grey-800; - font-size: 22px; - line-height: 28px; - padding-bottom: 14px; -} - -.device-form-test-modules { - overflow: auto; - min-height: 78px; -} - -.device-form-actions { - padding: 0; - min-height: 30px; -} - -.close-button { - color: $primary; -} - -.device-form-mac-address-error { - white-space: nowrap; -} - -.delete-button { - color: $primary; - margin-right: auto; -} - -.hidden { - display: none; -} diff --git a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts deleted file mode 100644 index 38ab05ffa..000000000 --- a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts +++ /dev/null @@ -1,211 +0,0 @@ -/** - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { - AbstractControl, - FormArray, - FormBuilder, - FormGroup, - Validators, -} from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; - -import { Device, TestModule } from '../../../../model/device'; -import { DeviceValidators } from './device.validators'; -import { Subject } from 'rxjs'; -import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; -import { DevicesStore } from '../../devices.store'; - -const MAC_ADDRESS_PATTERN = - '^[\\s]*[a-fA-F0-9]{2}(?:[:][a-fA-F0-9]{2}){5}[\\s]*$'; - -interface DialogData { - title?: string; - device?: Device; - devices: Device[]; - testModules: TestModule[]; -} - -export enum FormAction { - Delete = 'Delete', - Save = 'Save', -} - -export interface FormResponse { - device?: Device; - action: FormAction; -} - -@Component({ - selector: 'app-device-form', - templateUrl: './device-form.component.html', - styleUrls: ['./device-form.component.scss'], - providers: [DevicesStore], -}) -export class DeviceFormComponent - extends EscapableDialogComponent - implements OnInit, OnDestroy -{ - deviceForm!: FormGroup; - testModules: TestModule[] = []; - error$: BehaviorSubject = new BehaviorSubject( - null - ); - private destroy$: Subject = new Subject(); - - constructor( - public override dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: DialogData, - private fb: FormBuilder, - private deviceValidators: DeviceValidators, - private devicesStore: DevicesStore - ) { - super(dialogRef); - } - - get model() { - return this.deviceForm.get('model') as AbstractControl; - } - - get manufacturer() { - return this.deviceForm.get('manufacturer') as AbstractControl; - } - - get mac_addr() { - return this.deviceForm.get('mac_addr') as AbstractControl; - } - - get test_modules() { - return this.deviceForm.controls['test_modules'] as FormArray; - } - - ngOnInit() { - this.createDeviceForm(); - this.testModules = this.data.testModules; - if (this.data.device) { - this.model.setValue(this.data.device.model); - this.manufacturer.setValue(this.data.device.manufacturer); - this.mac_addr.setValue(this.data.device.mac_addr); - } - } - - ngOnDestroy() { - this.destroy$.next(true); - this.destroy$.unsubscribe(); - } - - delete(): void { - this.dialogRef.close({ action: FormAction.Delete } as FormResponse); - } - - cancel(): void { - this.dialogRef.close(); - } - - saveDevice() { - this.checkMandatoryFields(); - if (this.deviceForm.invalid) { - this.deviceForm.markAllAsTouched(); - return; - } - - if (this.isAllTestsDisabled()) { - this.error$.next( - 'At least one test has to be selected to save a Device.' - ); - return; - } - - const device = this.createDeviceFromForm(); - - this.updateDevice(device, () => { - this.dialogRef.close({ - action: FormAction.Save, - device, - } as FormResponse); - }); - } - - private updateDevice(device: Device, callback: () => void) { - if (this.data.device) { - this.devicesStore.editDevice({ - device, - mac_addr: this.data.device.mac_addr, - onSuccess: callback, - }); - } else { - this.devicesStore.saveDevice({ device, onSuccess: callback }); - } - } - - private isAllTestsDisabled(): boolean { - return this.deviceForm.value.test_modules.every((enabled: boolean) => { - return !enabled; - }); - } - - private createDeviceFromForm(): Device { - const testModules: { [key: string]: { enabled: boolean } } = {}; - this.deviceForm.value.test_modules.forEach( - (enabled: boolean, i: number) => { - testModules[this.testModules[i]?.name] = { - enabled: enabled, - }; - } - ); - return { - model: this.model.value.trim(), - manufacturer: this.manufacturer.value.trim(), - mac_addr: this.mac_addr.value.trim(), - test_modules: testModules, - } as Device; - } - - /** - * Model, manufacturer, MAC address are mandatory. - * It should be checked on submit. Other validation happens on blur. - */ - private checkMandatoryFields() { - this.setRequiredErrorIfEmpty(this.model); - this.setRequiredErrorIfEmpty(this.manufacturer); - this.setRequiredErrorIfEmpty(this.mac_addr); - } - - private setRequiredErrorIfEmpty(control: AbstractControl) { - if (!control.value.trim()) { - control.setErrors({ required: true }); - } - } - - private createDeviceForm() { - this.deviceForm = this.fb.group({ - model: ['', [this.deviceValidators.deviceStringFormat()]], - manufacturer: ['', [this.deviceValidators.deviceStringFormat()]], - mac_addr: [ - '', - [ - Validators.pattern(MAC_ADDRESS_PATTERN), - this.deviceValidators.differentMACAddress( - this.data.devices, - this.data.device - ), - ], - ], - test_modules: new FormArray([]), - }); - } -} diff --git a/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts b/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts index 2b7b23ae1..8ef530bad 100644 --- a/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts +++ b/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts @@ -22,8 +22,11 @@ import { Device } from '../../../../model/device'; * Validator uses for Device Name and Device Manufacturer inputs */ export class DeviceValidators { + static readonly STRING_FORMAT_MAX_LENGTH = 28; readonly STRING_FORMAT_REGEXP = new RegExp( - "^([a-z0-9\\p{L}\\p{M}.',-_ ]{1,28})$", + "^([a-z0-9\\p{L}\\p{M}.',-_ ]{1," + + DeviceValidators.STRING_FORMAT_MAX_LENGTH + + '})$', 'u' ); @@ -51,7 +54,20 @@ export class DeviceValidators { }; } - public differentMACAddress(devices: Device[], device?: Device): ValidatorFn { + public testModulesRequired(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (value.every((module: boolean) => !module)) { + return { required: true }; + } + return null; + }; + } + + public differentMACAddress( + devices: Device[], + device: Device | null + ): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const value = control.value?.trim(); if (value && (!device || device?.mac_addr !== value)) { diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html new file mode 100644 index 000000000..f2eb9edc8 --- /dev/null +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html @@ -0,0 +1,183 @@ + +
+
+ + + + + Please, check. The manufacturer name must be a maximum of 28 + characters. Only letters, numbers, and accented letters are + permitted. + + + Device Manufacturer is required + + + + + + + Please, check. The device model name must be a maximum of 28 + characters. Only letters, numbers, and accented letters are + permitted. + + + Device Model is required + + + + + + + MAC address is required + + + Please, check. A MAC address consists of 12 hexadecimal digits (0 to + 9, a to f, or A to F). + + + This MAC address is already used for another device in the + repository. + + + + Please, select the testing journey for device + + + + + + Device Qualification + + + + + Pilot Assessment + + + + + + + At least one test has to be selected to save a Device. + + + +
+
+
+
+ + +
+ +
diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss new file mode 100644 index 000000000..86852ea68 --- /dev/null +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss @@ -0,0 +1,180 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@use 'm3-theme' as *; +@use 'colors'; +@use 'variables'; +@use 'mixins'; + +$form-max-width: var(--mat-dialog-container-max-width); +$form-min-width: 285px; + +:host { + container-type: size; + container-name: qualification-form; + display: grid; + height: 100%; + background: colors.$surface; + border-radius: 8px; + box-shadow: + 0px 4px 8px 3px rgba(60, 64, 67, 0.15), + 0px 1px 3px 0px rgba(60, 64, 67, 0.3); + box-sizing: border-box; +} + +.device-qualification-form { + overflow-y: scroll; +} + +::ng-deep .device-form-test-modules { + overflow: auto; + min-height: 78px; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + grid-auto-flow: column; + padding-top: 16px; + padding-left: 10px; + p { + margin: 8px 0; + } +} + +.hidden { + display: none; +} + +.device-qualification-form-journey-label { + font-family: variables.$font-text; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.1px; + color: colors.$on-surface-variant; + padding: 20px 20px 8px 16px; +} + +.device-qualification-form-journey-button { + padding: 0 18px; +} + +.device-qualification-form-journey-button-info { + display: flex; +} + +.device-qualification-form-journey-button-label { + font-family: variables.$font-text; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.2px; +} + +.device-qualification-form-page { + display: grid; + align-content: start; + padding: 24px 60px 0 60px; +} + +.device-qualification-form-journey { + display: grid; + grid-template-columns: repeat(2, 1fr); +} + +.device-qualification-form-actions { + @include mixins.form-actions; + + div { + display: flex; + gap: 12px; + } + + .close-button { + padding: 0 16px; + } + + .delete-button:not(.mat-mdc-button-disabled) { + @include mixins.delete-red-button; + } + + .close-button:not(.mat-mdc-button-disabled) { + @include mixins.secondary-button; + } +} + +::ng-deep mat-error { + background: colors.$white; +} + +:host mat-form-field { + &::ng-deep.mat-mdc-form-field-error-wrapper { + margin-top: -20px; + position: static; + } +} + +::ng-deep .device-tests-description { + padding: 0 20px; +} + +::ng-deep .device-tests-title { + font-family: variables.$font-text; + font-style: normal; + font-weight: 500; + font-size: 16px !important; + line-height: 24px !important; + letter-spacing: 0.1px; + padding: 20px 20px 8px 16px; +} + +.device-qualification-form-test-modules-container-error + ::ng-deep + .device-tests-title { + color: colors.$red-800; +} + +.device-qualification-form-test-modules-error { + padding: 0 24px; +} + +@container qualification-form (height < 870px) { + .device-qualification-form-page { + overflow: scroll; + ::ng-deep app-device-tests { + overflow: visible; + } + } +} + +@container qualification-form (height < 580px) { + .device-qualification-form-page { + overflow: scroll; + .device-qualification-form-step-content { + overflow: visible; + } + } +} + +@container qualification-form (width < 360px) { + .manufacturer-field ::ng-deep mat-hint { + white-space: nowrap; + } + ::ng-deep .device-form-test-modules { + display: block; + } +} diff --git a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts similarity index 53% rename from modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts rename to modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts index 6ea72aa52..c366aec23 100644 --- a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts @@ -15,58 +15,90 @@ */ import { ComponentFixture, + discardPeriodicTasks, fakeAsync, - flush, TestBed, + tick, } from '@angular/core/testing'; -import { DeviceFormComponent, FormAction } from './device-form.component'; -import { MatButtonModule } from '@angular/material/button'; -import { FormControl, ReactiveFormsModule } from '@angular/forms'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatInputModule } from '@angular/material/input'; +import { DeviceQualificationFromComponent } from './device-qualification-from.component'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef, } from '@angular/material/dialog'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { Device } from '../../../../model/device'; import { of } from 'rxjs'; +import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; +import { MatButtonModule } from '@angular/material/button'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatInputModule } from '@angular/material/input'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { DeviceTestsComponent } from '../../../../components/device-tests/device-tests.component'; import { SpinnerComponent } from '../../../../components/spinner/spinner.component'; -import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; +import { + device, + DEVICES_FORM, + MOCK_TEST_MODULES, +} from '../../../../mocks/device.mock'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; +import { TestRunService } from '../../../../services/test-run.service'; import { DevicesStore } from '../../devices.store'; -import { device, MOCK_TEST_MODULES } from '../../../../mocks/device.mock'; -import SpyObj = jasmine.SpyObj; - -describe('DeviceFormComponent', () => { - let component: DeviceFormComponent; - let fixture: ComponentFixture; - let mockDevicesStore: SpyObj; +import { provideMockStore } from '@ngrx/store/testing'; +import { DeviceStatus, TestingType } from '../../../../model/device'; +import { Component, Input } from '@angular/core'; +import { QuestionFormat } from '../../../../model/question'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { selectDevices } from '../../../../store/selectors'; +import { SimpleDialogComponent } from '../../../../components/simple-dialog/simple-dialog.component'; + +describe('DeviceQualificationFromComponent', () => { + let component: DeviceQualificationFromComponent; + let fixture: ComponentFixture; let compiled: HTMLElement; - - beforeEach(() => { - mockDevicesStore = jasmine.createSpyObj('DevicesStore', [ - 'editDevice', + const testrunServiceMock: jasmine.SpyObj = + jasmine.createSpyObj('testrunServiceMock', [ + 'fetchQuestionnaireFormat', 'saveDevice', ]); + const keyboardEvent = new BehaviorSubject( + new KeyboardEvent('keydown', { code: '' }) + ); + + const MOCK_DEVICE = { + status: DeviceStatus.VALID, + manufacturer: 'manufacturer', + model: 'model', + mac_addr: '01:01:01:01:01:01', + test_pack: TestingType.Qualification, + type: '', + technology: '', + test_modules: { + udmi: { + enabled: true, + }, + connection: { + enabled: true, + }, + }, + additional_info: [ + { question: 'What type of device is this?', answer: '' }, + { + question: 'Does your device process any sensitive information? ', + answer: '', + }, + { + question: 'Please select the technology this device falls into', + answer: '', + }, + ], + }; - TestBed.configureTestingModule({ - declarations: [DeviceFormComponent], - providers: [ - { provide: DevicesStore, useValue: mockDevicesStore }, - { - provide: MatDialogRef, - useValue: { - keydownEvents: () => of(new KeyboardEvent('keydown', { code: '' })), - close: () => ({}), - }, - }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - provideNgxMask(), - ], + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FakeDynamicFormComponent], imports: [ + DeviceQualificationFromComponent, MatButtonModule, ReactiveFormsModule, MatCheckboxModule, @@ -77,120 +109,68 @@ describe('DeviceFormComponent', () => { SpinnerComponent, NgxMaskDirective, NgxMaskPipe, + MatIconTestingModule, ], - }); - TestBed.overrideProvider(DevicesStore, { useValue: mockDevicesStore }); + providers: [ + DevicesStore, + { + provide: MatDialogRef, + useValue: { + keydownEvents: () => keyboardEvent.asObservable(), + close: () => ({}), + }, + }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: TestRunService, useValue: testrunServiceMock }, + provideNgxMask(), + provideMockStore({ + selectors: [{ selector: selectDevices, value: [device, device] }], + }), + ], + }).compileComponents(); - fixture = TestBed.createComponent(DeviceFormComponent); + fixture = TestBed.createComponent(DeviceQualificationFromComponent); component = fixture.componentInstance; compiled = fixture.nativeElement as HTMLElement; - component.data = { - testModules: MOCK_TEST_MODULES, - devices: [], - }; - fixture.detectChanges(); + + fixture.componentRef.setInput('testModules', MOCK_TEST_MODULES); + fixture.componentRef.setInput('devices', []); + fixture.componentRef.setInput('isCreate', true); + + testrunServiceMock.fetchQuestionnaireFormat.and.returnValue( + of(DEVICES_FORM) + ); + + testrunServiceMock.saveDevice.and.returnValue(of(true)); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); it('should contain device form', () => { - const form = compiled.querySelector('.device-form'); + fixture.detectChanges(); + const form = compiled.querySelector('.device-qualification-form'); expect(form).toBeTruthy(); }); - it('should close dialog on "cancel" click', () => { - const closeSpy = spyOn(component.dialogRef, 'close'); - const closeButton = compiled.querySelector( - '.close-button' - ) as HTMLButtonElement; - - closeButton?.click(); - - expect(closeSpy).toHaveBeenCalledWith(); + it('should fetch devices format', () => { + const getQuestionnaireFormatSpy = spyOn( + component.devicesStore, + 'getQuestionnaireFormat' + ); + fixture.detectChanges(); - closeSpy.calls.reset(); + expect(getQuestionnaireFormatSpy).toHaveBeenCalled(); }); - it('should not save data when fields are empty', () => { - const saveButton = compiled.querySelector( - '.save-button' - ) as HTMLButtonElement; - const model: HTMLInputElement = compiled.querySelector( - '.device-form-model' - ) as HTMLInputElement; - const manufacturer: HTMLInputElement = compiled.querySelector( - '.device-form-manufacturer' - ) as HTMLInputElement; - const macAddress: HTMLInputElement = compiled.querySelector( - '.device-form-mac-address' - ) as HTMLInputElement; - - ['', ' '].forEach(value => { - model.value = value; - model.dispatchEvent(new Event('input')); - manufacturer.value = value; - manufacturer.dispatchEvent(new Event('input')); - macAddress.value = value; - macAddress.dispatchEvent(new Event('input')); - saveButton?.click(); + describe('test modules', () => { + beforeEach(() => { fixture.detectChanges(); - - const requiredErrors = compiled.querySelectorAll('mat-error'); - expect(requiredErrors?.length).toEqual(3); - - requiredErrors.forEach(error => { - expect(error?.innerHTML).toContain('required'); - }); }); - }); - - it('should not save data if no test selected', fakeAsync(() => { - component.model.setValue('model'); - component.manufacturer.setValue('manufacturer'); - component.mac_addr.setValue('07:07:07:07:07:07'); - component.test_modules.setValue([false, false]); - - component.saveDevice(); - fixture.detectChanges(); - - const error = compiled.querySelector('mat-error'); - expect(error?.innerHTML).toContain( - 'At least one test has to be selected to save a Device.' - ); - - flush(); - })); - - it('should save data when form is valid', () => { - const device: Device = { - manufacturer: 'manufacturer', - model: 'model', - mac_addr: '07:07:07:07:07:07', - test_modules: { - connection: { - enabled: true, - }, - udmi: { - enabled: false, - }, - }, - }; - component.model.setValue('model'); - component.manufacturer.setValue('manufacturer'); - component.mac_addr.setValue('07:07:07:07:07:07'); - - component.saveDevice(); - - const args = mockDevicesStore.saveDevice.calls.argsFor(0); - // @ts-expect-error config is in object - expect(args[0].device).toEqual(device); - expect(mockDevicesStore.saveDevice).toHaveBeenCalled(); - }); - describe('test modules', () => { it('should be present', () => { const test = compiled.querySelectorAll('mat-checkbox'); @@ -202,12 +182,33 @@ describe('DeviceFormComponent', () => { expect(tests[0].classList.contains('disabled')).toEqual(false); }); + + it('should have error when no modules selected', () => { + component.test_modules.setValue([false, false]); + component.test_modules.markAsTouched(); + fixture.detectChanges(); + const modules = compiled.querySelector( + '.device-qualification-form-test-modules-container-error' + ); + const error = compiled.querySelector( + '.device-qualification-form-test-modules-error' + ); + + expect(modules).toBeTruthy(); + expect(error?.innerHTML.trim()).toEqual( + 'At least one test has to be selected to save a Device.' + ); + }); }); describe('device model', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + it('should not contain errors when input is correct', () => { const model: HTMLInputElement = compiled.querySelector( - '.device-form-model' + '.device-qualification-form-model' ) as HTMLInputElement; ['model', 'Gebäude', 'jardín'].forEach(value => { model.value = value; @@ -228,7 +229,7 @@ describe('DeviceFormComponent', () => { 'as&@3$', ].forEach(value => { const model: HTMLInputElement = compiled.querySelector( - '.device-form-model' + '.device-qualification-form-model' ) as HTMLInputElement; model.value = value; model.dispatchEvent(new Event('input')); @@ -247,9 +248,13 @@ describe('DeviceFormComponent', () => { }); describe('device manufacturer', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + it('should not contain errors when input is correct', () => { const manufacturer: HTMLInputElement = compiled.querySelector( - '.device-form-manufacturer' + '.device-qualification-form-manufacturer' ) as HTMLInputElement; ['manufacturer', 'Gebäude', 'jardín'].forEach(value => { manufacturer.value = value; @@ -270,7 +275,7 @@ describe('DeviceFormComponent', () => { 'as&@3$', ].forEach(value => { const manufacturer: HTMLInputElement = compiled.querySelector( - '.device-form-manufacturer' + '.device-qualification-form-manufacturer' ) as HTMLInputElement; manufacturer.value = value; manufacturer.dispatchEvent(new Event('input')); @@ -291,12 +296,14 @@ describe('DeviceFormComponent', () => { describe('mac address', () => { it('should not be disabled', () => { + fixture.detectChanges(); expect(component.mac_addr.disabled).toBeFalse(); }); it('should not contain errors when input is correct', () => { + fixture.detectChanges(); const macAddress: HTMLInputElement = compiled.querySelector( - '.device-form-mac-address' + '.device-qualification-form-mac-address' ) as HTMLInputElement; ['07:07:07:07:07:07', ' 07:07:07:07:07:07 '].forEach(value => { macAddress.value = value; @@ -311,9 +318,10 @@ describe('DeviceFormComponent', () => { }); it('should have "pattern" error when field does not satisfy pattern', () => { + fixture.detectChanges(); ['value', 'q01e423573c4'].forEach(value => { const macAddress: HTMLInputElement = compiled.querySelector( - '.device-form-mac-address' + '.device-qualification-form-mac-address' ) as HTMLInputElement; macAddress.value = value; macAddress.dispatchEvent(new Event('input')); @@ -331,15 +339,14 @@ describe('DeviceFormComponent', () => { }); it('should have "has_same_mac_address" error when MAC address is already used', () => { - component.data = { - testModules: MOCK_TEST_MODULES, - devices: [device], - }; - component.ngOnInit(); + fixture.componentRef.setInput('testModules', MOCK_TEST_MODULES); + fixture.componentRef.setInput('devices', [device]); + fixture.componentRef.setInput('isCreate', true); + fixture.detectChanges(); const macAddress: HTMLInputElement = compiled.querySelector( - '.device-form-mac-address' + '.device-qualification-form-mac-address' ) as HTMLInputElement; macAddress.value = '00:1e:42:35:73:c4'; macAddress.dispatchEvent(new Event('input')); @@ -356,104 +363,142 @@ describe('DeviceFormComponent', () => { }); }); - it('should have hidden delete device button', () => { - const deleteButton = compiled.querySelector( - '.delete-button' - ) as HTMLButtonElement; - - expect(deleteButton.classList.contains('hidden')).toBeTrue(); - }); - describe('when device is present', () => { beforeEach(() => { - component.data = { - devices: [device], - testModules: MOCK_TEST_MODULES, - device: { - manufacturer: 'Delta', - model: 'O3-DIN-CPU', - mac_addr: '00:1e:42:35:73:c4', - test_modules: { - udmi: { - enabled: true, - }, + fixture.componentRef.setInput('testModules', MOCK_TEST_MODULES); + fixture.componentRef.setInput('devices', [device]); + fixture.componentRef.setInput('isCreate', false); + fixture.componentRef.setInput('initialDevice', { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: 'O3-DIN-CPU', + mac_addr: '00:1e:42:35:73:c4', + test_modules: { + udmi: { + enabled: true, }, }, - }; - component.ngOnInit(); - fixture.detectChanges(); + }); }); - it('should fill form values with device values', () => { + it('should fill form values with device values', fakeAsync(() => { + fixture.detectChanges(); + + testrunServiceMock.fetchQuestionnaireFormat.and.returnValue( + of(DEVICES_FORM) + ); + + tick(1); + const model: HTMLInputElement = compiled.querySelector( - '.device-form-model' + '.device-qualification-form-model' ) as HTMLInputElement; const manufacturer: HTMLInputElement = compiled.querySelector( - '.device-form-manufacturer' + '.device-qualification-form-manufacturer' ) as HTMLInputElement; expect(model.value).toEqual('O3-DIN-CPU'); expect(manufacturer.value).toEqual('Delta'); - }); - it('should save data even mac address already exist', fakeAsync(() => { - // fill the test controls - component.test_modules.push(new FormControl(false)); - component.test_modules.push(new FormControl(true)); - component.saveDevice(); + discardPeriodicTasks(); + })); + + it('should have enabled delete button', () => { fixture.detectChanges(); + const button = compiled.querySelector( + '.delete-button' + ) as HTMLButtonElement; - fixture.whenStable().then(() => { - const error = compiled.querySelector('mat-error'); - expect(error).toBeFalse(); - }); + expect(button.disabled).toBeFalse(); + }); - const args = mockDevicesStore.editDevice.calls.argsFor(0); - // @ts-expect-error config is in object - expect(args[0].device).toEqual({ - manufacturer: 'Delta', + it('should open cancel dialog when device is changed', () => { + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ + beforeClosed: () => of(true), + } as MatDialogRef); + fixture.detectChanges(); + fixture.componentRef.setInput('initialDevice', { + status: DeviceStatus.VALID, + manufacturer: 'Alpha', model: 'O3-DIN-CPU', - mac_addr: '00:1e:42:35:73:c4', + mac_addr: '00:22:42:35:73:c4', test_modules: { - connection: { - enabled: false, - }, udmi: { enabled: true, }, }, }); - expect(mockDevicesStore.editDevice).toHaveBeenCalled(); + fixture.detectChanges(); - flush(); - })); + expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, { + ariaLabel: 'Discard the Device changes', + data: { + title: 'Discard changes?', + content: `You have unsaved changes that would be permanently lost.`, + confirmName: 'Discard', + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: ['simple-dialog', 'close-device'], + }); - it('should have delete device button', () => { - const deleteButton = compiled.querySelector( - '.delete-button' - ) as HTMLButtonElement; + openSpy.calls.reset(); + }); + }); - expect(deleteButton.classList.contains('hidden')).toBeFalse(); - expect(deleteButton).toBeTruthy(); + describe('when device is null', () => { + beforeEach(() => { + fixture.componentRef.setInput('testModules', MOCK_TEST_MODULES); + fixture.componentRef.setInput('devices', [device]); + fixture.componentRef.setInput('isCreate', true); + fixture.componentRef.setInput('initialDevice', null); }); - it('should close dialog with delete action on "delete" click', () => { - const closeSpy = spyOn(component.dialogRef, 'close'); - const closeButton = compiled.querySelector( + it('should have disabled delete button', () => { + fixture.detectChanges(); + const button = compiled.querySelector( '.delete-button' ) as HTMLButtonElement; - closeButton?.click(); + expect(button.disabled).toBeTrue(); + }); + }); - expect(closeSpy).toHaveBeenCalledWith({ action: FormAction.Delete }); + describe('with changes', () => { + it('should have enabled cancel button', () => { + fixture.detectChanges(); + component.model.setValue('new value'); + fixture.detectChanges(); + const button = compiled.querySelector( + '.close-button' + ) as HTMLButtonElement; - closeSpy.calls.reset(); + expect(button.disabled).toBeFalse(); }); }); - it('should has loader element', () => { - const spinner = compiled.querySelector('app-spinner'); - - expect(spinner).toBeTruthy(); + describe('onSaveClicked', () => { + it('should emit device', () => { + fixture.detectChanges(); + const saveSpy = spyOn(component.save, 'emit'); + component.manufacturer.setValue('manufacturer'); + component.model.setValue('model'); + component.mac_addr.setValue('01:01:01:01:01:01'); + component.deviceQualificationForm.markAsDirty(); + + component.onSaveClicked(); + expect(saveSpy).toHaveBeenCalledWith(MOCK_DEVICE); + }); }); }); + +@Component({ + selector: 'app-dynamic-form', + template: '
', + standalone: false, +}) +class FakeDynamicFormComponent { + @Input() format: QuestionFormat[] = []; + @Input() optionKey: string | undefined; +} diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts new file mode 100644 index 000000000..49e655492 --- /dev/null +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts @@ -0,0 +1,496 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Component, + OnInit, + inject, + input, + effect, + output, + ChangeDetectorRef, + AfterViewInit, +} from '@angular/core'; +import { + AbstractControl, + FormArray, + FormBuilder, + FormGroup, + ReactiveFormsModule, + ValidatorFn, + Validators, +} from '@angular/forms'; +import { DeviceValidators } from '../device-form/device.validators'; +import { + Device, + DeviceStatus, + DeviceView, + TestingType, + TestModule, +} from '../../../../model/device'; +import { CommonModule } from '@angular/common'; +import { + MatError, + MatFormField, + MatFormFieldModule, +} from '@angular/material/form-field'; +import { DeviceTestsComponent } from '../../../../components/device-tests/device-tests.component'; +import { MatButtonModule } from '@angular/material/button'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { TextFieldModule } from '@angular/cdk/text-field'; +import { NgxMaskDirective, provideNgxMask } from 'ngx-mask'; +import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; +import { ProfileValidators } from '../../../risk-assessment/profile-form/profile.validators'; +import { DevicesStore } from '../../devices.store'; +import { DynamicFormComponent } from '../../../../components/dynamic-form/dynamic-form.component'; +import { skip, timer } from 'rxjs'; +import { Question } from '../../../../model/profile'; +import { FormControlType, QuestionFormat } from '../../../../model/question'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { SimpleDialogComponent } from '../../../../components/simple-dialog/simple-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { of } from 'rxjs/internal/observable/of'; +import { Observable } from 'rxjs/internal/Observable'; +import { map } from 'rxjs/internal/operators/map'; + +const MAC_ADDRESS_PATTERN = + '^[\\s]*[a-fA-F0-9]{2}(?:[:][a-fA-F0-9]{2}){5}[\\s]*$'; + +@Component({ + selector: 'app-device-qualification-from', + + imports: [ + MatFormField, + DeviceTestsComponent, + MatButtonModule, + CommonModule, + ReactiveFormsModule, + MatInputModule, + MatError, + MatFormFieldModule, + MatSelectModule, + MatCheckboxModule, + TextFieldModule, + NgxMaskDirective, + MatRadioGroup, + MatRadioButton, + DynamicFormComponent, + ], + providers: [provideNgxMask()], + templateUrl: './device-qualification-from.component.html', + styleUrl: './device-qualification-from.component.scss', +}) +export class DeviceQualificationFromComponent implements OnInit, AfterViewInit { + readonly TestingType = TestingType; + readonly DeviceView = DeviceView; + + private fb = inject(FormBuilder); + private cdr = inject(ChangeDetectorRef); + private deviceValidators = inject(DeviceValidators); + private profileValidators = inject(ProfileValidators); + private changeDevice = false; + private macAddressValidator!: ValidatorFn; + + dialog = inject(MatDialog); + formIsLoaded$ = new BehaviorSubject(false); + devicesStore = inject(DevicesStore); + deviceQualificationForm: FormGroup = this.fb.group({}); + format: QuestionFormat[] = []; + typeQuestion = 0; + technologyQuestion = 2; + device: Device | null = null; + + initialDevice = input(null); + + initialDeviceEffect = effect(() => { + if ( + this.changeDevice || + this.deviceHasNoChanges(this.device, this.createDeviceFromForm()) + ) { + this.changeDevice = false; + this.device = this.initialDevice(); + if (this.device && this.device.mac_addr) { + this.fillDeviceForm(this.format, this.device); + } else { + this.resetForm(); + } + this.updateMacAddressValidator(); + } else if (this.device != this.initialDevice()) { + // prevent select new device before user confirmation + this.devicesStore.selectDevice(this.device); + this.openCloseDialogToChangeDevice(this.initialDevice()); + } + }); + + devices = input([]); + testModules = input([]); + isCreate = input(true); + + save = output(); + delete = output(); + cancel = output(); + + get model() { + return this.deviceQualificationForm.get('model') as AbstractControl; + } + + get manufacturer() { + return this.deviceQualificationForm.get('manufacturer') as AbstractControl; + } + + get mac_addr() { + return this.deviceQualificationForm.get('mac_addr') as AbstractControl; + } + + get test_pack() { + return this.deviceQualificationForm.get('test_pack') as AbstractControl; + } + + get type() { + return this.deviceQualificationForm.get( + this.typeQuestion.toString() + ) as AbstractControl; + } + + get technology() { + return this.deviceQualificationForm.get( + this.technologyQuestion.toString() + ) as AbstractControl; + } + + get test_modules() { + return this.deviceQualificationForm.controls['test_modules'] as FormArray; + } + + ngOnInit(): void { + this.createDeviceForm(); + + this.devicesStore.questionnaireFormat$.pipe(skip(1)).subscribe(format => { + this.format = format; + + format.forEach((question, index) => { + // need to define the step and index of type and technology + if (question.question.toLowerCase().includes('type')) { + this.typeQuestion = index; + } else if (question.question.toLowerCase().includes('technology')) { + this.technologyQuestion = index; + } + }); + + timer(0).subscribe(() => { + if (this.initialDevice()) { + this.fillDeviceForm(this.format, this.initialDevice()!); + } + this.formIsLoaded$.next(true); + }); + }); + + this.devicesStore.getQuestionnaireFormat(); + } + + ngAfterViewInit() { + this.cdr.detectChanges(); + } + + onSaveClicked(): void { + this.save.emit(this.createDeviceFromForm()); + this.deviceQualificationForm.markAsPristine(); + this.changeDevice = true; + } + + onCancelClicked(): void { + this.cancel.emit(); + } + + onDeleteClick(): void { + this.delete.emit(this.initialDevice()!); + } + + resetForm() { + this.deviceQualificationForm.reset({ + test_pack: TestingType.Qualification, + }); + } + + createDeviceFromForm(): Device { + const testModules: { [key: string]: { enabled: boolean } } = {}; + this.deviceQualificationForm.value.test_modules.forEach( + (enabled: boolean, i: number) => { + testModules[this.testModules()[i]?.name] = { + enabled: enabled, + }; + } + ); + + const additionalInfo: Question[] = []; + + this.format.forEach((question, index) => { + const response: Question = {}; + response.question = question.question; + + if (question.type === FormControlType.SELECT_MULTIPLE) { + const answer: number[] = []; + question.options?.forEach((_, idx) => { + const value = this.deviceQualificationForm.value[index][idx]; + if (value) { + answer.push(idx); + } + }); + response.answer = answer; + } else { + response.answer = this.deviceQualificationForm.value[index]?.trim(); + } + additionalInfo.push(response); + }); + + return { + status: DeviceStatus.VALID, + model: this.model?.value?.trim(), + manufacturer: this.manufacturer?.value?.trim(), + mac_addr: this.mac_addr?.value?.trim(), + test_pack: this.test_pack?.value, + test_modules: testModules, + type: this.type?.value, + technology: this.technology?.value, + additional_info: additionalInfo, + } as Device; + } + + deviceHasNoChanges(device1: Device | null, device2: Device) { + return ( + (device1 === null && this.deviceIsEmpty(device2)) || + (device1 && this.compareDevices(device1, device2)) + ); + } + + close(): Observable { + if ( + this.deviceHasNoChanges(this.initialDevice(), this.createDeviceFromForm()) + ) { + return of(true); + } + return this.openCloseDialog().pipe(map(res => !!res)); + } + + private deviceIsEmpty(device: Device) { + if (device.manufacturer && device.manufacturer !== '') { + return false; + } + if (device.model && device.model !== '') { + return false; + } + if (device.mac_addr && device.mac_addr !== '') { + return false; + } + if (device.type && device.type !== '') { + return false; + } + if (device.technology && device.technology !== '') { + return false; + } + if (device.test_pack !== TestingType.Qualification) { + return false; + } + const keys1 = Object.keys(device.test_modules!); + + for (const key of keys1) { + const val1 = device.test_modules![key]; + if (!val1.enabled) { + return false; + } + } + if (device.additional_info) { + for (const question of device.additional_info) { + if (question.answer && question.answer !== '') { + return false; + } + } + } else { + return false; + } + return true; + } + + private compareDevices(device1: Device, device2: Device) { + if (device1.manufacturer !== device2.manufacturer) { + return false; + } + if (device1.model !== device2.model) { + return false; + } + if (device1.mac_addr !== device2.mac_addr) { + return false; + } + if (device1.type !== device2.type) { + return false; + } + if (device1.technology !== device2.technology) { + return false; + } + if (device1.test_pack !== device2.test_pack) { + return false; + } + const keys1 = Object.keys(device1.test_modules!); + + for (const key of keys1) { + const val1 = device1.test_modules![key]; + const val2 = device2.test_modules![key]; + if (val1?.enabled !== val2?.enabled) { + return false; + } + } + + if (device1.additional_info) { + for (const question of device1.additional_info) { + if ( + question.answer !== + device2.additional_info?.find( + question2 => question2.question === question.question + )?.answer + ) { + return false; + } + } + } else { + return false; + } + return true; + } + + private fillDeviceForm(format: QuestionFormat[], device: Device): void { + format.forEach((question, index) => { + const answer = device.additional_info?.find( + answers => answers.question === question.question + )?.answer; + if (answer !== undefined && answer !== null && answer !== '') { + if (question.type === FormControlType.SELECT_MULTIPLE) { + question.options?.forEach((item, idx) => { + if ((answer as number[])?.includes(idx)) { + ( + this.deviceQualificationForm.get(index.toString()) as FormGroup + ).controls[idx].setValue(true); + } else { + ( + this.deviceQualificationForm.get(index.toString()) as FormGroup + ).controls[idx].setValue(false); + } + }); + } else { + ( + this.deviceQualificationForm.get( + index.toString() + ) as AbstractControl + ).setValue(answer || ''); + } + } else { + ( + this.deviceQualificationForm.get(index.toString()) as AbstractControl + )?.markAsTouched(); + } + }); + this.model.setValue(device.model); + this.manufacturer.setValue(device.manufacturer); + this.mac_addr.setValue(device.mac_addr); + + if ( + device.test_pack && + (device.test_pack === TestingType.Qualification || + device.test_pack === TestingType.Pilot) + ) { + this.test_pack.setValue(device.test_pack); + } else { + this.test_pack.setValue(TestingType.Qualification); + } + + this.type?.setValue(device.type); + this.technology?.setValue(device.technology); + } + + private createDeviceForm() { + this.macAddressValidator = this.deviceValidators.differentMACAddress( + this.devices(), + this.initialDevice() + ); + + this.deviceQualificationForm = this.fb.group({ + model: [ + '', + [ + this.profileValidators.textRequired(), + this.deviceValidators.deviceStringFormat(), + ], + ], + manufacturer: [ + '', + [ + this.profileValidators.textRequired(), + this.deviceValidators.deviceStringFormat(), + ], + ], + mac_addr: [ + '', + [ + this.profileValidators.textRequired(), + Validators.pattern(MAC_ADDRESS_PATTERN), + this.macAddressValidator, + ], + ], + test_modules: new FormArray( + [], + this.deviceValidators.testModulesRequired() + ), + test_pack: [TestingType.Qualification], + }); + } + + private openCloseDialogToChangeDevice(device: Device | null) { + this.openCloseDialog().subscribe(close => { + if (close) { + this.changeDevice = true; + this.devicesStore.selectDevice(device); + } + }); + } + private openCloseDialog() { + const dialogRef = this.dialog.open(SimpleDialogComponent, { + ariaLabel: 'Discard the Device changes', + data: { + title: 'Discard changes?', + content: `You have unsaved changes that would be permanently lost.`, + confirmName: 'Discard', + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: ['simple-dialog', 'close-device'], + }); + + return dialogRef?.beforeClosed(); + } + + private updateMacAddressValidator() { + if (this.mac_addr) { + this.mac_addr.removeValidators([this.macAddressValidator]); + this.macAddressValidator = this.deviceValidators.differentMACAddress( + this.devices(), + this.initialDevice() + ); + this.mac_addr.addValidators(this.macAddressValidator); + this.mac_addr.updateValueAndValidity(); + } + } +} diff --git a/modules/ui/src/app/pages/devices/devices.component.html b/modules/ui/src/app/pages/devices/devices.component.html index c9f5d3aee..c04c2cc3c 100644 --- a/modules/ui/src/app/pages/devices/devices.component.html +++ b/modules/ui/src/app/pages/devices/devices.component.html @@ -14,40 +14,67 @@ limitations under the License. --> - - -

Devices

- -
-
- - - -
-
+ + + + + + + + + -
+ -
+
+ + + +
diff --git a/modules/ui/src/app/pages/devices/devices.component.scss b/modules/ui/src/app/pages/devices/devices.component.scss index a089fb978..a031674bd 100644 --- a/modules/ui/src/app/pages/devices/devices.component.scss +++ b/modules/ui/src/app/pages/devices/devices.component.scss @@ -13,34 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import 'src/theming/colors'; -@import 'src/theming/variables'; +@use 'mixins'; -:host { - overflow: hidden; - flex-direction: column; - display: flex; -} - -.device-repository-content-empty { - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.device-repository-toolbar { - gap: 16px; - background: $white; - height: 74px; - padding: 24px 0 8px 32px; -} - -.device-repository-content { - align-content: start; - padding: 24px 32px; - display: grid; - grid-template-columns: repeat(auto-fit, $device-item-width); - gap: 16px; - overflow-y: auto; +.device-add-button { + @include mixins.add-button; } diff --git a/modules/ui/src/app/pages/devices/devices.component.spec.ts b/modules/ui/src/app/pages/devices/devices.component.spec.ts index ad4aa5759..f693d8103 100644 --- a/modules/ui/src/app/pages/devices/devices.component.spec.ts +++ b/modules/ui/src/app/pages/devices/devices.component.spec.ts @@ -20,15 +20,10 @@ import { tick, } from '@angular/core/testing'; import { of } from 'rxjs'; -import { Device } from '../../model/device'; +import { Device, DeviceAction, TestModule } from '../../model/device'; import { DevicesComponent } from './devices.component'; -import { DevicesModule } from './devices.module'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { - DeviceFormComponent, - FormAction, -} from './components/device-form/device-form.component'; import { MatDialogRef } from '@angular/material/dialog'; import { device, MOCK_TEST_MODULES } from '../../mocks/device.mock'; import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; @@ -40,8 +35,9 @@ import { TestrunInitiateFormComponent } from '../testrun/components/testrun-init import { Routes } from '../../model/routes'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { Component } from '@angular/core'; +import { Component, input, output } from '@angular/core'; import { MOCK_PROGRESS_DATA_IN_PROGRESS } from '../../mocks/testrun.mock'; +import { DeviceQualificationFromComponent } from './components/device-qualification-from/device-qualification-from.component'; describe('DevicesComponent', () => { let component: DevicesComponent; @@ -59,27 +55,37 @@ describe('DevicesComponent', () => { mockDevicesStore = jasmine.createSpyObj('DevicesStore', [ 'setIsOpenAddDevice', 'selectDevice', - 'deleteDevice', 'setStatus', + 'getTestModules', + 'deleteDevice', ]); - - mockDevicesStore.testModules = MOCK_TEST_MODULES; + mockDevicesStore.isOpenAddDevice$ = of(false); await TestBed.configureTestingModule({ imports: [ RouterTestingModule.withRoutes([ { path: 'testing', component: FakeProgressComponent }, ]), - DevicesModule, BrowserAnimationsModule, MatIconTestingModule, + DevicesComponent, + FakeProgressComponent, + FakeDeviceQualificationComponent, ], providers: [ { provide: DevicesStore, useValue: mockDevicesStore }, { provide: FocusManagerService, useValue: stateServiceMock }, ], - declarations: [DevicesComponent, FakeProgressComponent], - }).compileComponents(); + }) + .overrideComponent(DevicesComponent, { + remove: { + imports: [DeviceQualificationFromComponent], + }, + add: { + imports: [FakeDeviceQualificationComponent], + }, + }) + .compileComponents(); TestBed.overrideProvider(DevicesStore, { useValue: mockDevicesStore }); @@ -99,26 +105,31 @@ describe('DevicesComponent', () => { devices: [] as Device[], selectedDevice: null, deviceInProgress: null, + testModules: [], + actions: [ + { + action: DeviceAction.StartNewTestrun, + svgIcon: 'testrun_logo_small', + }, + { action: DeviceAction.Delete, icon: 'delete' }, + ], }); mockDevicesStore.devices$ = of([]); - mockDevicesStore.isOpenAddDevice$ = of(true); + mockDevicesStore.testModules$ = of([]); fixture.detectChanges(); }); it('should show only add device button if no device added', () => { - const button = compiled.querySelector( - '.device-repository-content-empty button' - ); + const button = compiled.querySelector('app-empty-page button'); expect(button).toBeTruthy(); }); - it('should open the modal if isOpenAddDevice$ as true', () => { - const openDialogSpy = spyOn(component, 'openDialog'); - + it('should open form if isOpenAddDevice$ as true', () => { + mockDevicesStore.isOpenAddDevice$ = of(true); component.ngOnInit(); - expect(openDialogSpy).toHaveBeenCalled(); + expect(component.isOpenDeviceForm).toBeTrue(); }); }); @@ -128,6 +139,14 @@ describe('DevicesComponent', () => { devices: [device, device, device], selectedDevice: device, deviceInProgress: device, + testModules: [], + actions: [ + { + action: DeviceAction.StartNewTestrun, + svgIcon: 'testrun_logo_small', + }, + { action: DeviceAction.Delete, icon: 'delete' }, + ], }); fixture.detectChanges(); }); @@ -138,92 +157,15 @@ describe('DevicesComponent', () => { expect(item.length).toEqual(3); })); - it('should add device-item-selected class for selected device', fakeAsync(() => { - const item = compiled.querySelector('app-device-item'); - - expect(item?.classList.contains('device-item-selected')).toBeTrue(); - })); - - it('should open device dialog on "add device button" click', () => { - const openSpy = spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(true), - } as MatDialogRef); + it('should open form on "add device button" click', () => { fixture.detectChanges(); const button = compiled.querySelector( - '.device-add-button' + '.add-entity-button' ) as HTMLButtonElement; button?.click(); expect(button).toBeTruthy(); - expect(openSpy).toHaveBeenCalled(); - expect(openSpy).toHaveBeenCalledWith(DeviceFormComponent, { - ariaLabel: 'Create device', - data: { - device: null, - title: 'Create device', - testModules: MOCK_TEST_MODULES, - devices: [device, device, device], - }, - autoFocus: true, - hasBackdrop: true, - disableClose: true, - panelClass: 'device-form-dialog', - }); - - openSpy.calls.reset(); - }); - - describe('#openDialog', () => { - it('should open device dialog on item click', () => { - const openSpy = spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(true), - } as MatDialogRef); - fixture.detectChanges(); - - component.openDialog([device], device); - - expect(openSpy).toHaveBeenCalled(); - expect(openSpy).toHaveBeenCalledWith(DeviceFormComponent, { - ariaLabel: 'Edit device', - data: { - device: device, - title: 'Edit device', - devices: [device], - testModules: MOCK_TEST_MODULES, - }, - autoFocus: true, - hasBackdrop: true, - disableClose: true, - panelClass: 'device-form-dialog', - }); - - openSpy.calls.reset(); - }); - - it('should open device dialog with delete-button focus element', () => { - const openSpy = spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(true), - } as MatDialogRef); - fixture.detectChanges(); - - component.openDialog([device], device, true); - - expect(openSpy).toHaveBeenCalledWith(DeviceFormComponent, { - ariaLabel: 'Edit device', - data: { - device: device, - title: 'Edit device', - devices: [device], - testModules: MOCK_TEST_MODULES, - }, - autoFocus: '.delete-button', - hasBackdrop: true, - disableClose: true, - panelClass: 'device-form-dialog', - }); - - openSpy.calls.reset(); - }); + expect(component.isOpenDeviceForm).toBeTrue(); }); it('should disable device if deviceInProgress is exist', () => { @@ -233,36 +175,20 @@ describe('DevicesComponent', () => { }); }); - it('should call setIsOpenAddDevice if dialog closes with null', () => { - spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(null), - } as MatDialogRef); - - component.openDialog(); - - expect(mockDevicesStore.setIsOpenAddDevice).toHaveBeenCalled(); - }); - - it('should delete device if dialog closes with object, action delete and selected device', () => { - spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => - of({ - device, - action: FormAction.Delete, - }), - } as MatDialogRef); - - component.openDialog([device], device); - - expect(mockDevicesStore.deleteDevice).toHaveBeenCalled(); - }); - - describe('delete device dialog', () => { + describe('close dialog', () => { beforeEach(() => { component.viewModel$ = of({ devices: [device, device, device], selectedDevice: device, deviceInProgress: null, + testModules: [], + actions: [ + { + action: DeviceAction.StartNewTestrun, + svgIcon: 'testrun_logo_small', + }, + { action: DeviceAction.Delete, icon: 'delete' }, + ], }); fixture.detectChanges(); }); @@ -272,30 +198,38 @@ describe('DevicesComponent', () => { expect(item.length).toEqual(3); })); + }); + + describe('delete device dialog', () => { + beforeEach(() => { + component.viewModel$ = of({ + devices: [device, device, device], + selectedDevice: device, + deviceInProgress: null, + testModules: [], + actions: [ + { + action: DeviceAction.StartNewTestrun, + svgIcon: 'testrun_logo_small', + }, + { action: DeviceAction.Delete, icon: 'delete' }, + ], + }); + fixture.detectChanges(); + }); it('should delete device when dialog return true', () => { spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(true), + beforeClosed: () => of(true), } as MatDialogRef); - component.openDeleteDialog([device], device); + component.openDeleteDialog(device); const args = mockDevicesStore.deleteDevice.calls.argsFor(0); // @ts-expect-error config is in object expect(args[0].device).toEqual(device); expect(mockDevicesStore.deleteDevice).toHaveBeenCalled(); }); - - it('should open device dialog when dialog return null', () => { - const openDeviceDialogSpy = spyOn(component, 'openDialog'); - spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(null), - } as MatDialogRef); - - component.openDeleteDialog([device], device); - - expect(openDeviceDialogSpy).toHaveBeenCalledWith([device], device, true); - }); }); describe('#openStartTestrun', () => { @@ -305,25 +239,30 @@ describe('DevicesComponent', () => { } as MatDialogRef); fixture.ngZone?.run(() => { - component.openStartTestrun(device, [device]); + component.openStartTestrun(device, [device], MOCK_TEST_MODULES); expect(openSpy).toHaveBeenCalledWith(TestrunInitiateFormComponent, { ariaLabel: 'Initiate testrun', data: { devices: [device], device: device, + testModules: MOCK_TEST_MODULES, }, - autoFocus: true, + autoFocus: 'dialog', hasBackdrop: true, disableClose: true, panelClass: 'initiate-test-run-dialog', }); - tick(); + tick(100); + expect(router.url).toBe(Routes.Testing); expect(mockDevicesStore.setStatus).toHaveBeenCalledWith( MOCK_PROGRESS_DATA_IN_PROGRESS ); + expect( + stateServiceMock.focusFirstElementInContainer + ).toHaveBeenCalled(); openSpy.calls.reset(); }); @@ -336,3 +275,18 @@ describe('DevicesComponent', () => { template: '', }) class FakeProgressComponent {} + +@Component({ + selector: 'app-device-qualification-from', + template: '
', +}) +class FakeDeviceQualificationComponent { + initialDevice = input(null); + devices = input([]); + testModules = input([]); + isCreate = input(true); + + save = output(); + delete = output(); + cancel = output(); +} diff --git a/modules/ui/src/app/pages/devices/devices.component.ts b/modules/ui/src/app/pages/devices/devices.component.ts index b886fcfc8..73553e9a1 100644 --- a/modules/ui/src/app/pages/devices/devices.component.ts +++ b/modules/ui/src/app/pages/devices/devices.component.ts @@ -19,53 +19,97 @@ import { OnDestroy, OnInit, ChangeDetectorRef, + inject, + viewChild, } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { Device, DeviceView } from '../../model/device'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { - DeviceFormComponent, - FormAction, - FormResponse, -} from './components/device-form/device-form.component'; -import { Subject, takeUntil } from 'rxjs'; + Device, + DeviceAction, + DeviceView, + TestModule, +} from '../../model/device'; +import { LayoutType } from '../../model/layout-type'; +import { Subject, takeUntil, timer } from 'rxjs'; import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; -import { combineLatest } from 'rxjs/internal/observable/combineLatest'; import { FocusManagerService } from '../../services/focus-manager.service'; import { Routes } from '../../model/routes'; import { Router } from '@angular/router'; -import { timer } from 'rxjs/internal/observable/timer'; import { TestrunInitiateFormComponent } from '../testrun/components/testrun-initiate-form/testrun-initiate-form.component'; import { DevicesStore } from './devices.store'; +import { DeviceQualificationFromComponent } from './components/device-qualification-from/device-qualification-from.component'; +import { CommonModule } from '@angular/common'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatInputModule } from '@angular/material/input'; +import { DeviceItemComponent } from '../../components/device-item/device-item.component'; +import { EmptyPageComponent } from '../../components/empty-page/empty-page.component'; +import { ListLayoutComponent } from '../../components/list-layout/list-layout.component'; +import { EntityActionResult } from '../../model/entity-action'; +import { NoEntitySelectedComponent } from '../../components/no-entity-selected/no-entity-selected.component'; +import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { CanComponentDeactivate } from '../../guards/can-deactivate.guard'; +import { Observable } from 'rxjs/internal/Observable'; +import { of } from 'rxjs/internal/observable/of'; @Component({ selector: 'app-device-repository', templateUrl: './devices.component.html', styleUrls: ['./devices.component.scss'], + imports: [ + CommonModule, + MatToolbarModule, + MatButtonModule, + MatIconModule, + ScrollingModule, + MatDialogModule, + ReactiveFormsModule, + MatCheckboxModule, + MatInputModule, + DeviceItemComponent, + EmptyPageComponent, + ListLayoutComponent, + NoEntitySelectedComponent, + DeviceQualificationFromComponent, + ], providers: [DevicesStore], }) -export class DevicesComponent implements OnInit, OnDestroy { +export class DevicesComponent + implements OnInit, OnDestroy, CanComponentDeactivate +{ readonly DeviceView = DeviceView; + readonly LayoutType = LayoutType; + readonly form = viewChild(DeviceQualificationFromComponent); + private readonly focusManagerService = inject(FocusManagerService); + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly liveAnnouncer = inject(LiveAnnouncer); + private readonly route = inject(Router); + private readonly devicesStore = inject(DevicesStore); + dialog = inject(MatDialog); + private element = inject(ElementRef); private destroy$: Subject = new Subject(); viewModel$ = this.devicesStore.viewModel$; + isOpenDeviceForm = false; - constructor( - private readonly focusManagerService: FocusManagerService, - public dialog: MatDialog, - private element: ElementRef, - private readonly changeDetectorRef: ChangeDetectorRef, - private route: Router, - private devicesStore: DevicesStore - ) {} + canDeactivate(): Observable { + const form = this.form(); + if (form) { + return form.close(); + } else { + return of(true); + } + } ngOnInit(): void { - combineLatest([ - this.devicesStore.devices$, - this.devicesStore.isOpenAddDevice$, - ]) + this.devicesStore.isOpenAddDevice$ .pipe(takeUntil(this.destroy$)) - .subscribe(([devices, isOpenAddDevice]) => { - if (!devices?.length && isOpenAddDevice) { - this.openDialog(devices); + .subscribe(isOpenAddDevice => { + if (isOpenAddDevice) { + this.openForm(); } }); } @@ -75,14 +119,34 @@ export class DevicesComponent implements OnInit, OnDestroy { this.destroy$.unsubscribe(); } - openStartTestrun(selectedDevice: Device, devices: Device[]): void { + menuItemClicked( + { action, entity }: EntityActionResult, + devices: Device[], + testModules: TestModule[] + ) { + switch (action) { + case DeviceAction.StartNewTestrun: + this.openStartTestrun(entity, devices, testModules); + break; + case DeviceAction.Delete: + this.openDeleteDialog(entity); + break; + } + } + + openStartTestrun( + selectedDevice: Device, + devices: Device[], + testModules: TestModule[] + ): void { const dialogRef = this.dialog.open(TestrunInitiateFormComponent, { ariaLabel: 'Initiate testrun', data: { devices, device: selectedDevice, + testModules, }, - autoFocus: true, + autoFocus: 'dialog', hasBackdrop: true, disableClose: true, panelClass: 'initiate-test-run-dialog', @@ -98,58 +162,56 @@ export class DevicesComponent implements OnInit, OnDestroy { window.dataLayer.push({ event: 'successful_testrun_initiation', }); - this.route.navigate([Routes.Testing]); + this.route.navigate([Routes.Testing]).then(() => + timer(100).subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(); + }) + ); } }); } - openDialog( - devices: Device[] = [], - selectedDevice?: Device, - focusDeleteButton = false - ): void { - const dialogRef = this.dialog.open(DeviceFormComponent, { - ariaLabel: selectedDevice ? 'Edit device' : 'Create device', - data: { - device: selectedDevice || null, - title: selectedDevice ? 'Edit device' : 'Create device', - testModules: this.devicesStore.testModules, - devices, - }, - autoFocus: focusDeleteButton ? '.delete-button' : true, - hasBackdrop: true, - disableClose: true, - panelClass: 'device-form-dialog', + async openForm(device: Device | null = null) { + this.devicesStore.selectDevice(device); + this.isOpenDeviceForm = true; + await this.liveAnnouncer.announce('Device qualification form'); + this.focusManagerService.focusFirstElementInContainer( + window.document.querySelector('app-device-qualification-from') + ); + } + + save(device: Device, initialDevice: Device | null) { + this.updateDevice(device, initialDevice, (index: number) => { + this.devicesStore.selectDevice(device); + this.focusDevice(index); }); + } - dialogRef - ?.afterClosed() - .pipe(takeUntil(this.destroy$)) - .subscribe((response: FormResponse) => { - this.devicesStore.selectDevice(null); - if (!response) { - this.devicesStore.setIsOpenAddDevice(false); - return; - } - if ( - response.action === FormAction.Save && - response.device && - !selectedDevice - ) { - timer(10) - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.focusManagerService.focusFirstElementInContainer(); - }); - } - if (response.action === FormAction.Delete && selectedDevice) { - this.devicesStore.selectDevice(selectedDevice); - this.openDeleteDialog(devices, selectedDevice); - } + discard() { + this.openCloseDialog(); + } + + delete(device: Device) { + this.openDeleteDialog(device); + } + + private updateDevice( + device: Device, + initialDevice: Device | null = null, + callback: (idx: number) => void + ) { + if (initialDevice) { + this.devicesStore.editDevice({ + device, + mac_addr: initialDevice.mac_addr, + onSuccess: callback, }); + } else { + this.devicesStore.saveDevice({ device, onSuccess: callback }); + } } - openDeleteDialog(devices: Device[], device: Device) { + openDeleteDialog(device: Device) { const dialogRef = this.dialog.open(SimpleDialogComponent, { ariaLabel: 'Delete device', data: { @@ -157,46 +219,105 @@ export class DevicesComponent implements OnInit, OnDestroy { content: `You are about to delete ${ device.manufacturer + ' ' + device.model }. Are you sure?`, - device: device, }, autoFocus: true, hasBackdrop: true, disableClose: true, - panelClass: 'simple-dialog', + panelClass: ['simple-dialog', 'delete-dialog'], + }); + dialogRef?.beforeClosed().subscribe(deleteDevice => { + if (deleteDevice) { + this.devicesStore.deleteDevice({ + device: device, + onDelete: (deviceIndex = 0) => { + this.isOpenDeviceForm = false; + this.focusNextButton(deviceIndex); + }, + }); + } }); + } - dialogRef - ?.afterClosed() - .pipe(takeUntil(this.destroy$)) - .subscribe(deleteDevice => { - if (deleteDevice) { - this.devicesStore.deleteDevice({ - device, - onDelete: () => { - this.focusNextButton(); - this.devicesStore.selectDevice(null); - }, - }); - } else { - this.openDialog(devices, device, true); - this.devicesStore.selectDevice(null); - } - }); + deviceIsDisabled(mac_addr?: string) { + return (device: Device) => { + return device.mac_addr === mac_addr; + }; + } + + getDeviceTooltip(mac_addr?: string) { + return (device: Device) => { + if (this.deviceIsDisabled(mac_addr)(device)) { + return 'Device under test'; + } + return ''; + }; } - private focusNextButton() { - // Try to focus next device item, if exitst - const next = this.element.nativeElement.querySelector( - '.device-item-selected + app-device-item button' + private openCloseDialog() { + const dialogRef = this.dialog.open(SimpleDialogComponent, { + ariaLabel: 'Discard the Device changes', + data: { + title: 'Discard changes?', + content: `You have unsaved changes that would be permanently lost.`, + confirmName: 'Discard', + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: ['simple-dialog', 'discard-dialog'], + }); + + dialogRef?.beforeClosed().subscribe(close => { + if (close) { + this.isOpenDeviceForm = false; + this.devicesStore.selectDevice(null); + this.focusSelectedButton(); + } + }); + } + + private focusSelectedButton() { + const selectedButton = this.element.nativeElement.querySelector( + 'app-device-item.selected .button-edit' ); + if (selectedButton) { + selectedButton.focus(); + } else { + this.focusAddButton(); + } + } + + private focusNextButton(index: number) { + this.changeDetectorRef.detectChanges(); + // Try to focus next device item, if exist + const next = this.element.nativeElement.querySelectorAll( + 'app-device-item .button-edit' + )[index]; if (next) { next.focus(); } else { - this.changeDetectorRef.detectChanges(); // If next device item doest not exist, add device button should be focused - const addButton = + this.focusAddButton(); + } + } + + private focusDevice(index: number) { + this.changeDetectorRef.detectChanges(); + const device = this.element.nativeElement.querySelectorAll( + 'app-device-item .button-edit' + )[index]; + device?.focus(); + } + + private focusAddButton(): void { + let addButton = + this.element.nativeElement.querySelector('.add-entity-button'); + if (!addButton) { + addButton = this.element.nativeElement.querySelector('.device-add-button'); - addButton?.focus(); } + timer(100).subscribe(() => { + addButton?.focus(); + }); } } diff --git a/modules/ui/src/app/pages/devices/devices.module.ts b/modules/ui/src/app/pages/devices/devices.module.ts deleted file mode 100644 index e0110eca3..000000000 --- a/modules/ui/src/app/pages/devices/devices.module.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { ScrollingModule } from '@angular/cdk/scrolling'; -import { CommonModule } from '@angular/common'; -import { HttpClientModule } from '@angular/common/http'; -import { NgModule } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { DeviceFormComponent } from './components/device-form/device-form.component'; - -import { DevicesRoutingModule } from './devices-routing.module'; -import { DevicesComponent } from './devices.component'; -import { DeviceItemComponent } from '../../components/device-item/device-item.component'; -import { DeviceTestsComponent } from '../../components/device-tests/device-tests.component'; -import { SpinnerComponent } from '../../components/spinner/spinner.component'; -import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; -import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; - -@NgModule({ - declarations: [DevicesComponent, DeviceFormComponent], - imports: [ - CommonModule, - DevicesRoutingModule, - MatToolbarModule, - MatButtonModule, - MatIconModule, - ScrollingModule, - HttpClientModule, - MatDialogModule, - ReactiveFormsModule, - MatCheckboxModule, - MatInputModule, - DeviceItemComponent, - DeviceTestsComponent, - SpinnerComponent, - SimpleDialogComponent, - NgxMaskDirective, - NgxMaskPipe, - ], - providers: [provideNgxMask()], -}) -export class DevicesModule {} diff --git a/modules/ui/src/app/pages/devices/devices.store.spec.ts b/modules/ui/src/app/pages/devices/devices.store.spec.ts index 4d2f1bd20..e355963e6 100644 --- a/modules/ui/src/app/pages/devices/devices.store.spec.ts +++ b/modules/ui/src/app/pages/devices/devices.store.spec.ts @@ -20,6 +20,7 @@ import { AppState } from '../../store/state'; import { selectDeviceInProgress, selectHasDevices, + selectTestModules, } from '../../store/selectors'; import { TestRunService } from '../../services/test-run.service'; import SpyObj = jasmine.SpyObj; @@ -32,6 +33,7 @@ import { import { selectDevices, selectIsOpenAddDevice } from '../../store/selectors'; import { DevicesStore } from './devices.store'; import { MOCK_PROGRESS_DATA_IN_PROGRESS } from '../../mocks/testrun.mock'; +import { DeviceAction } from '../../model/device'; describe('DevicesStore', () => { let devicesStore: DevicesStore; @@ -54,6 +56,7 @@ describe('DevicesStore', () => { { selector: selectDevices, value: [device] }, { selector: selectIsOpenAddDevice, value: true }, { selector: selectDeviceInProgress, value: device }, + { selector: selectTestModules, value: [] }, ], }), { provide: TestRunService, useValue: mockService }, @@ -89,6 +92,14 @@ describe('DevicesStore', () => { devices: [device], selectedDevice: null, deviceInProgress: device, + testModules: [], + actions: [ + { + action: DeviceAction.StartNewTestrun, + svgIcon: 'testrun_logo_small', + }, + { action: DeviceAction.Delete, icon: 'delete' }, + ], }); done(); }); diff --git a/modules/ui/src/app/pages/devices/devices.store.ts b/modules/ui/src/app/pages/devices/devices.store.ts index 7145aabca..64e994f1c 100644 --- a/modules/ui/src/app/pages/devices/devices.store.ts +++ b/modules/ui/src/app/pages/devices/devices.store.ts @@ -14,18 +14,19 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { ComponentStore } from '@ngrx/component-store'; import { TestRunService } from '../../services/test-run.service'; import { exhaustMap } from 'rxjs'; import { tap, withLatestFrom } from 'rxjs/operators'; -import { Device } from '../../model/device'; +import { Device, DeviceAction, TestModule } from '../../model/device'; import { AppState } from '../../store/state'; import { Store } from '@ngrx/store'; import { selectDeviceInProgress, selectDevices, selectIsOpenAddDevice, + selectTestModules, } from '../../store/selectors'; import { fetchSystemStatusSuccess, @@ -33,24 +34,36 @@ import { setIsOpenAddDevice, } from '../../store/actions'; import { TestrunStatus } from '../../model/testrun-status'; +import { EntityAction } from '../../model/entity-action'; +import { QuestionFormat } from '../../model/question'; export interface DevicesComponentState { devices: Device[]; selectedDevice: Device | null; + testModules: TestModule[]; + questionnaireFormat: QuestionFormat[]; + actions: EntityAction[]; } @Injectable() export class DevicesStore extends ComponentStore { + private testRunService = inject(TestRunService); + private store = inject>(Store); + devices$ = this.store.select(selectDevices); isOpenAddDevice$ = this.store.select(selectIsOpenAddDevice); + testModules$ = this.store.select(selectTestModules); + questionnaireFormat$ = this.select(state => state.questionnaireFormat); private deviceInProgress$ = this.store.select(selectDeviceInProgress); private selectedDevice$ = this.select(state => state.selectedDevice); + private actions$ = this.select(state => state.actions); - testModules = this.testRunService.getTestModules(); viewModel$ = this.select({ devices: this.devices$, selectedDevice: this.selectedDevice$, deviceInProgress: this.deviceInProgress$, + testModules: this.testModules$, + actions: this.actions$, }); selectDevice = this.updater((state, device: Device | null) => ({ @@ -58,9 +71,15 @@ export class DevicesStore extends ComponentStore { selectedDevice: device, })); + updateQuestionnaireFormat = this.updater( + (state, questionnaireFormat: QuestionFormat[]) => ({ + ...state, + questionnaireFormat, + }) + ); deleteDevice = this.effect<{ device: Device; - onDelete: () => void; + onDelete: (idx: number) => void; }>(trigger$ => { return trigger$.pipe( exhaustMap(({ device, onDelete }) => { @@ -68,8 +87,11 @@ export class DevicesStore extends ComponentStore { withLatestFrom(this.devices$), tap(([deleted, devices]) => { if (deleted) { + const idx = devices.findIndex( + item => device.mac_addr === item.mac_addr + ); this.removeDevice(device, devices); - onDelete(); + onDelete(idx); } }) ); @@ -77,28 +99,29 @@ export class DevicesStore extends ComponentStore { ); }); - saveDevice = this.effect<{ device: Device; onSuccess: () => void }>( - trigger$ => { - return trigger$.pipe( - exhaustMap(({ device, onSuccess }) => { - return this.testRunService.saveDevice(device).pipe( - withLatestFrom(this.devices$), - tap(([added, devices]) => { - if (added) { - this.addDevice(device, devices); - onSuccess(); - } - }) - ); - }) - ); - } - ); + saveDevice = this.effect<{ + device: Device; + onSuccess: (idx: number) => void; + }>(trigger$ => { + return trigger$.pipe( + exhaustMap(({ device, onSuccess }) => { + return this.testRunService.saveDevice(device).pipe( + withLatestFrom(this.devices$), + tap(([added, devices]) => { + if (added) { + this.addDevice(device, devices); + onSuccess(0); + } + }) + ); + }) + ); + }); editDevice = this.effect<{ device: Device; mac_addr: string; - onSuccess: () => void; + onSuccess: (idx: number) => void; }>(trigger$ => { return trigger$.pipe( exhaustMap(({ device, mac_addr, onSuccess }) => { @@ -106,8 +129,11 @@ export class DevicesStore extends ComponentStore { withLatestFrom(this.devices$), tap(([edited, devices]) => { if (edited) { + const idx = devices.findIndex( + item => device.mac_addr === item.mac_addr + ); this.updateDevice(device, mac_addr, devices); - onSuccess(); + onSuccess(idx); } }) ); @@ -135,8 +161,20 @@ export class DevicesStore extends ComponentStore { ); }); + getQuestionnaireFormat = this.effect(trigger$ => { + return trigger$.pipe( + exhaustMap(() => { + return this.testRunService.fetchQuestionnaireFormat().pipe( + tap((questionnaireFormat: QuestionFormat[]) => { + this.updateQuestionnaireFormat(questionnaireFormat); + }) + ); + }) + ); + }); + private addDevice(device: Device, devices: Device[]): void { - this.updateDevices(devices.concat([device])); + this.updateDevices([device, ...devices]); } private updateDevice( @@ -167,13 +205,16 @@ export class DevicesStore extends ComponentStore { this.store.dispatch(setDevices({ devices })); } - constructor( - private testRunService: TestRunService, - private store: Store - ) { + constructor() { super({ devices: [], selectedDevice: null, + testModules: [], + questionnaireFormat: [], + actions: [ + { action: DeviceAction.StartNewTestrun, svgIcon: 'testrun_logo_small' }, + { action: DeviceAction.Delete, icon: 'delete' }, + ], }); } } diff --git a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.html b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.html similarity index 73% rename from modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.html rename to modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.html index 7157db8d3..ea6194595 100644 --- a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.html +++ b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.html @@ -1,19 +1,22 @@ - -

- {{ description }} -

+
+ +

+ {{ description }} +

+
+ - {{ label }} @@ -35,3 +38,6 @@ +
+ +
diff --git a/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.scss b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.scss new file mode 100644 index 000000000..e17e02add --- /dev/null +++ b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.scss @@ -0,0 +1,104 @@ +@use '@angular/material' as mat; +@use 'colors'; +@use 'variables'; + +:host { + display: grid; + grid-template-columns: 1fr 1fr 1.1fr; + gap: 46px; + padding: 8px 0; +} + +.setting-form-label { + font-size: 16px; + line-height: 24px; + font-weight: 500; + color: colors.$on-surface; +} + +:host:has(.two-ports-message) .internet-label { + padding-top: 16px; +} + +.setting-label-description { + margin: 0; + font-size: 14px; + line-height: 20px; + letter-spacing: 0; + color: colors.$on-surface-variant; +} + +.setting-option-value { + padding: 8px 16px; +} + +.option-value { + margin: 0; + font-family: variables.$font-text; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 22px; + letter-spacing: 0; + + &.top { + color: colors.$on-surface-variant; + } + + &.bottom { + color: colors.$schemes-outline; + } +} + +.setting-field { + width: 100%; + padding: 10px 0; + + &.mat-form-field-disabled { + opacity: 0.6; + } + + &::ng-deep.mat-mdc-form-field-subscript-wrapper { + display: none; + } + + ::ng-deep .mat-mdc-form-field-infix { + min-height: 60px; + height: 60px; + display: flex; + align-items: center; + padding-top: 9px; + padding-bottom: 6px; + } + + ::ng-deep .mat-mdc-floating-label { + font-family: Roboto; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.2px; + } + + ::ng-deep .mat-mdc-floating-label:not(.mdc-floating-label--float-above) { + top: 28px; + } +} + +.label-column { + display: flex; + flex-direction: column; + justify-content: center; + padding: 0 16px; + color: colors.$on-surface-variant; + font-family: variables.$font-text; +} + +::ng-deep .info-column { + display: flex; + flex-direction: column; + gap: 6px; + width: 260px; + padding: 0 12px; + justify-content: center; +} diff --git a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.spec.ts b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.spec.ts similarity index 88% rename from modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.spec.ts rename to modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.spec.ts index b662b0a40..b3a022509 100644 --- a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.spec.ts +++ b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.spec.ts @@ -7,16 +7,19 @@ import { FormsModule, ReactiveFormsModule, } from '@angular/forms'; -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; @Component({ template: '
' + '
', + standalone: false, }) class DummyComponent { + private readonly fb = inject(FormBuilder); + public testForm!: FormGroup; - constructor(private readonly fb: FormBuilder) { + constructor() { this.testForm = this.fb.group({ test: [''], }); diff --git a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.ts b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.ts similarity index 98% rename from modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.ts rename to modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.ts index 1fa20d0cb..233f84cbd 100644 --- a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.ts +++ b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.ts @@ -18,7 +18,7 @@ import { KeyValuePipe, NgForOf, NgIf } from '@angular/common'; @Component({ selector: 'app-settings-dropdown', - standalone: true, + imports: [ ReactiveFormsModule, MatFormFieldModule, diff --git a/modules/ui/src/app/pages/general-settings/general-settings.component.html b/modules/ui/src/app/pages/general-settings/general-settings.component.html new file mode 100644 index 000000000..9b95aab53 --- /dev/null +++ b/modules/ui/src/app/pages/general-settings/general-settings.component.html @@ -0,0 +1,210 @@ + +
+ + To change settings, you need to stop testing. + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
+ + Both interfaces must have different values + + +
+ +
+
+
+ + Warning! No ports detected. + +
+
+ +
+
+
+ + +
+
+

{{ label }}

+

+ {{ description }} +

+
+
+ + + + + + +
+
+

+ By installing and running Testrun, you understand and accept the Terms + of Service found + here +

+
+
+
+ +

+ Single port +

+

+ Two ports +

+
+ +

+ + Opt out from Google Analytics + +

+
+ +

+ Internet port is disabled because you selected single port mode +

+
+ + +

This port is required

+ +
+
+ +

+ If a port is missing from this list, you can + + Refresh + + the System settings +

+
+
+ diff --git a/modules/ui/src/app/pages/general-settings/general-settings.component.scss b/modules/ui/src/app/pages/general-settings/general-settings.component.scss new file mode 100644 index 000000000..d476e3b20 --- /dev/null +++ b/modules/ui/src/app/pages/general-settings/general-settings.component.scss @@ -0,0 +1,127 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@use 'colors'; +@use 'variables'; + +:host { + display: flex; + flex-direction: column; + flex: 1 0 auto; + padding-top: 4px; +} + +.setting-drawer-content { + overflow-y: scroll; + height: max-content; + padding: 8px 32px; + + .setting-drawer-content-form-empty { + grid-template-rows: repeat(2, auto) 1fr; + } + ::ng-deep .callout-container { + margin: 10px 0; + } +} + +.error-message-container { + display: block; + margin-top: auto; + padding-bottom: 8px; + text-align: center; +} + +.message { + margin: 0; + color: colors.$on-surface-variant; + font-family: variables.$font-text; + font-size: 14px; + line-height: 20px; + letter-spacing: 0; +} + +.setting-drawer-footer { + padding: 16px 40px; + margin-top: auto; + display: flex; + flex-shrink: 0; + justify-content: flex-end; + + .save-button { + padding: 0 24px; + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.25px; + &::ng-deep .mat-focus-indicator { + display: none; + } + } +} + +.settings-drawer-header-button:not(.mat-mdc-button-disabled), +.close-button:not(.mat-mdc-button-disabled), +.save-button:not(.mat-mdc-button-disabled) { + cursor: pointer; + pointer-events: auto; +} + +.section-item { + min-height: 88px; + display: grid; + grid-template-columns: 1fr 1fr 1.1fr; + gap: 46px; + padding: 8px 0; + + p { + margin: 0; + } + + .label-column { + display: flex; + flex-direction: column; + justify-content: center; + padding: 0 16px; + color: colors.$on-surface-variant; + font-family: variables.$font-text; + } + + .data-column { + display: flex; + } + + .setting-form-label, + .setting-data { + font-size: 16px; + line-height: 24px; + font-weight: 500; + color: colors.$on-surface; + } + + .setting-data { + display: flex; + align-items: center; + font-weight: 400; + } + + .setting-label-description { + margin: 0; + font-size: 14px; + line-height: 20px; + letter-spacing: 0; + color: colors.$on-surface-variant; + } +} diff --git a/modules/ui/src/app/pages/general-settings/general-settings.component.spec.ts b/modules/ui/src/app/pages/general-settings/general-settings.component.spec.ts new file mode 100644 index 000000000..ceef25c51 --- /dev/null +++ b/modules/ui/src/app/pages/general-settings/general-settings.component.spec.ts @@ -0,0 +1,343 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GeneralSettingsComponent } from './general-settings.component'; +import { of } from 'rxjs'; +import { MatRadioModule } from '@angular/material/radio'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIcon, MatIconModule } from '@angular/material/icon'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; +import { Component, Input } from '@angular/core'; +import SpyObj = jasmine.SpyObj; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { provideMockStore } from '@ngrx/store/testing'; +import { LoaderService } from '../../services/loader.service'; +import { GeneralSettingsStore } from './general-settings.store'; +import { + MOCK_INTERFACES, + MOCK_SYSTEM_CONFIG_WITH_DATA, +} from '../../mocks/settings.mock'; +import { SettingsDropdownComponent } from './components/settings-dropdown/settings-dropdown.component'; +import { CalloutComponent } from '../../components/callout/callout.component'; +import { SpinnerComponent } from '../../components/spinner/spinner.component'; +import { TestRunService } from '../../services/test-run.service'; +import { MOCK_PROGRESS_DATA_COMPLIANT } from '../../mocks/testrun.mock'; + +describe('GeneralSettingsComponent', () => { + let component: GeneralSettingsComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + let mockLoaderService: SpyObj; + let mockTestRunService: SpyObj; + let mockSettingsStore: SpyObj; + + beforeEach(async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window).gtag = jasmine.createSpy('gtag'); + mockLoaderService = jasmine.createSpyObj('LoaderService', ['setLoading']); + mockTestRunService = jasmine.createSpyObj('TesRunService', [ + 'testrunInProgress', + ]); + mockSettingsStore = jasmine.createSpyObj('SettingsStore', [ + 'getInterfaces', + 'updateSystemConfig', + 'setIsSubmitting', + 'setDefaultFormValues', + 'setFormDisable', + 'setFormEnable', + 'getSystemConfig', + 'viewModel$', + 'systemStatus$', + ]); + mockSettingsStore.systemStatus$ = of(MOCK_PROGRESS_DATA_COMPLIANT); + + await TestBed.configureTestingModule({ + providers: [ + { provide: LoaderService, useValue: mockLoaderService }, + { provide: TestRunService, useValue: mockTestRunService }, + { provide: GeneralSettingsStore, useValue: mockSettingsStore }, + provideMockStore(), + ], + imports: [ + GeneralSettingsComponent, + BrowserAnimationsModule, + MatButtonModule, + MatIconModule, + MatRadioModule, + ReactiveFormsModule, + MatIconTestingModule, + MatIcon, + MatInputModule, + MatSelectModule, + SettingsDropdownComponent, + FakeSpinnerComponent, + FakeCalloutComponent, + ], + }) + .overrideComponent(GeneralSettingsComponent, { + remove: { + imports: [CalloutComponent, SpinnerComponent], + }, + add: { + imports: [FakeSpinnerComponent, FakeCalloutComponent], + }, + }) + .compileComponents(); + + TestBed.overrideProvider(GeneralSettingsStore, { + useValue: mockSettingsStore, + }); + + fixture = TestBed.createComponent(GeneralSettingsComponent); + + component = fixture.componentInstance; + component.viewModel$ = of({ + systemConfig: { network: {} }, + hasConnectionSettings: false, + isSubmitting: false, + isLessThanOneInterface: false, + interfaces: {}, + deviceOptions: {}, + internetOptions: {}, + logLevelOptions: {}, + monitoringPeriodOptions: {}, + }); + fixture.detectChanges(); + compiled = fixture.nativeElement as HTMLElement; + + component.ngOnInit(); + }); + + afterEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window).gtag = undefined; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('#reloadSetting should call setLoading in loaderService', () => { + component.reloadSetting(); + + expect(mockLoaderService.setLoading).toHaveBeenCalledWith(true); + }); + + describe('#settingsDisable', () => { + it('should disable setting form when get settingDisable as true ', () => { + component.settingsDisable = true; + + expect(mockSettingsStore.setFormDisable).toHaveBeenCalled(); + }); + + it('should enable setting form when get settingDisable as false ', () => { + component.settingsDisable = false; + + expect(mockSettingsStore.setFormEnable).toHaveBeenCalled(); + }); + + it('should disable "Save" button when get settingDisable as true', () => { + component.settingsDisable = true; + + const saveBtn = compiled.querySelector( + '.save-button' + ) as HTMLButtonElement; + + expect(saveBtn.disabled).toBeTrue(); + }); + }); + + describe('#saveSetting', () => { + beforeEach(() => { + component.ngOnInit(); + }); + + it('should have form error if form has the same value', () => { + const mockSameValue = { key: 'sameValue' }; + component.deviceControl.setValue(mockSameValue); + component.internetControl.setValue(mockSameValue); + + component.saveSetting(); + + expect(component.settingForm.invalid).toBeTrue(); + expect(component.isFormError).toBeTrue(); + expect(mockSettingsStore.setIsSubmitting).toHaveBeenCalledWith(true); + }); + + it('should call createSystemConfig when setting form valid', () => { + const expectedResult = { + network: { + device_intf: 'mockDeviceKey', + internet_intf: '', + }, + log_level: 'INFO', + monitor_period: 600, + }; + + component.deviceControl.setValue({ + key: 'mockDeviceKey', + value: 'mockDeviceValue', + }); + + component.internetControl.setValue({ + key: '', + value: 'defaultValue', + }); + + component.logLevel.setValue({ + key: 'INFO', + value: '', + }); + + component.monitorPeriod.setValue({ + key: '600', + value: '', + }); + + component.saveSetting(); + + const args = mockSettingsStore.updateSystemConfig.calls.argsFor(0); + // @ts-expect-error config is in object + expect(args[0].config).toEqual(expectedResult); + expect(component.settingForm.invalid).toBeFalse(); + expect(mockSettingsStore.updateSystemConfig).toHaveBeenCalled(); + }); + }); + + describe('with no interfaces data', () => { + beforeEach(() => { + component.viewModel$ = of({ + systemConfig: { network: {} }, + hasConnectionSettings: false, + isSubmitting: false, + isLessThanOneInterface: false, + interfaces: {}, + deviceOptions: {}, + internetOptions: {}, + logLevelOptions: {}, + monitoringPeriodOptions: {}, + }); + fixture.detectChanges(); + }); + + it('should have callout component', () => { + const callout = compiled.querySelector('app-callout'); + + expect(callout).toBeTruthy(); + }); + + it('should have disabled "Save" button', () => { + const saveBtn = compiled.querySelector( + '.save-button' + ) as HTMLButtonElement; + + expect(saveBtn.disabled).toBeTrue(); + }); + }); + + describe('with interfaces length less than one', () => { + beforeEach(() => { + component.viewModel$ = of({ + systemConfig: { network: {} }, + hasConnectionSettings: false, + isSubmitting: false, + isLessThanOneInterface: true, + interfaces: {}, + deviceOptions: {}, + internetOptions: {}, + logLevelOptions: {}, + monitoringPeriodOptions: {}, + }); + fixture.detectChanges(); + }); + + it('should have disabled "Save" button', () => { + component.deviceControl.setValue( + MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.device_intf + ); + component.internetControl.setValue( + MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.internet_intf + ); + fixture.detectChanges(); + + const saveBtn = compiled.querySelector( + '.save-button' + ) as HTMLButtonElement; + + expect(saveBtn.disabled).toBeTrue(); + }); + }); + + describe('with interfaces length more then one', () => { + beforeEach(() => { + component.viewModel$ = of({ + systemConfig: { network: {} }, + hasConnectionSettings: false, + isSubmitting: false, + isLessThanOneInterface: false, + interfaces: MOCK_INTERFACES, + deviceOptions: MOCK_INTERFACES, + internetOptions: MOCK_INTERFACES, + logLevelOptions: {}, + monitoringPeriodOptions: {}, + }); + fixture.detectChanges(); + }); + + it('should not have callout component', () => { + const callout = compiled.querySelector('app-callout'); + + expect(callout).toBeFalsy(); + }); + + it('should not have disabled "Save" button', () => { + component.deviceControl.setValue({ + key: MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.device_intf, + value: 'value', + }); + component.internetControl.setValue({ + key: MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.internet_intf, + value: 'value', + }); + component.settingForm.markAsDirty(); + fixture.detectChanges(); + + const saveBtn = compiled.querySelector( + '.save-button' + ) as HTMLButtonElement; + + expect(saveBtn.disabled).toBeFalse(); + }); + }); +}); + +@Component({ + selector: 'app-spinner', + template: '
', +}) +class FakeSpinnerComponent {} + +@Component({ + selector: 'app-callout', + template: '
', +}) +class FakeCalloutComponent { + @Input() type = ''; +} diff --git a/modules/ui/src/app/pages/general-settings/general-settings.component.ts b/modules/ui/src/app/pages/general-settings/general-settings.component.ts new file mode 100644 index 000000000..819f2af13 --- /dev/null +++ b/modules/ui/src/app/pages/general-settings/general-settings.component.ts @@ -0,0 +1,297 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { Subject, takeUntil, tap, timer } from 'rxjs'; +import { OnlyDifferentValuesValidator } from './only-different-values.validator'; +import { CalloutType } from '../../model/callout-type'; +import { FormKey, SystemConfig } from '../../model/setting'; +import { GeneralSettingsStore } from './general-settings.store'; +import { LoaderService } from '../../services/loader.service'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatInputModule } from '@angular/material/input'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { SpinnerComponent } from '../../components/spinner/spinner.component'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatButtonModule } from '@angular/material/button'; +import { SettingsDropdownComponent } from './components/settings-dropdown/settings-dropdown.component'; +import { MatSelectModule } from '@angular/material/select'; +import { MatIconModule } from '@angular/material/icon'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatDividerModule } from '@angular/material/divider'; +import { CalloutComponent } from '../../components/callout/callout.component'; +import { CommonModule } from '@angular/common'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { TestRunService } from '../../services/test-run.service'; +import { Router } from '@angular/router'; +import { Routes } from '../../model/routes'; +import { FocusManagerService } from '../../services/focus-manager.service'; +import { LocalStorageService } from '../../services/local-storage.service'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +declare const gtag: Function; + +@Component({ + selector: 'app-general-settings', + templateUrl: './general-settings.component.html', + styleUrls: ['./general-settings.component.scss'], + imports: [ + MatButtonModule, + MatIconModule, + MatToolbarModule, + MatSidenavModule, + MatButtonToggleModule, + MatRadioModule, + MatInputModule, + MatSelectModule, + MatTooltipModule, + ReactiveFormsModule, + MatFormFieldModule, + MatSnackBarModule, + MatDividerModule, + MatCheckbox, + FormsModule, + SpinnerComponent, + CalloutComponent, + SettingsDropdownComponent, + CommonModule, + ], + providers: [GeneralSettingsStore], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class GeneralSettingsComponent implements OnInit, OnDestroy { + private readonly fb = inject(FormBuilder); + private readonly onlyDifferentValuesValidator = inject( + OnlyDifferentValuesValidator + ); + private settingsStore = inject(GeneralSettingsStore); + private readonly loaderService = inject(LoaderService); + private readonly focusManagerService = inject(FocusManagerService); + private readonly localStorageService = inject(LocalStorageService); + + private isSettingsDisable = false; + get settingsDisable(): boolean { + return this.isSettingsDisable; + } + set settingsDisable(value: boolean) { + this.isSettingsDisable = value; + if (value) { + this.disableSettings(); + } else { + this.enableSettings(); + } + } + public readonly CalloutType = CalloutType; + public readonly FormKey = FormKey; + public settingForm!: FormGroup; + public analyticsForm!: FormGroup; + public readonly Routes = Routes; + viewModel$ = this.settingsStore.viewModel$; + + private destroy$: Subject = new Subject(); + private testRunService = inject(TestRunService); + private route = inject(Router); + + get deviceControl(): FormControl { + return this.settingForm.get(FormKey.DEVICE) as FormControl; + } + + get internetControl(): FormControl { + return this.settingForm.get(FormKey.INTERNET) as FormControl; + } + + get logLevel(): FormControl { + return this.settingForm.get(FormKey.LOG_LEVEL) as FormControl; + } + + get monitorPeriod(): FormControl { + return this.settingForm.get(FormKey.MONITOR_PERIOD) as FormControl; + } + + get isFormValues(): boolean { + return ( + this.deviceControl?.value?.value && + (this.isInternetControlDisabled || this.internetControl?.value?.value) + ); + } + + get isInternetControlDisabled(): boolean { + return this.internetControl?.disabled; + } + + get isFormError(): boolean { + return this.settingForm.hasError('hasSameValues'); + } + + ngOnInit() { + this.createSettingForm(); + this.createAnalyticsForm(); + this.cleanFormErrorMessage(); + this.settingsStore.getInterfaces(); + this.getSystemConfig(); + this.setDefaultFormValues(); + this.settingsStore.systemStatus$ + .pipe(takeUntil(this.destroy$)) + .subscribe(systemStatus => { + if (systemStatus?.status) { + const isTestrunInProgress = this.testRunService.testrunInProgress( + systemStatus.status + ); + if (isTestrunInProgress !== this.isSettingsDisable) { + this.settingsDisable = isTestrunInProgress; + } + } + }); + } + + navigateToRuntime(): void { + this.route.navigate([Routes.Testing]); + } + + reloadSetting(): void { + this.showLoading(); + this.getSystemInterfaces(); + this.getSystemConfig(); + this.setDefaultFormValues(); + } + + saveSetting(): void { + if (this.settingForm.invalid) { + this.settingsStore.setIsSubmitting(true); + this.settingForm.markAllAsTouched(); + } else { + this.createSystemConfig(); + this.settingForm.markAsPristine(); + this.analyticsForm.markAsPristine(); + this.setFocus(); + } + } + + private setFocus(): void { + timer(200).subscribe(() => { + const helpTip = window.document.querySelector( + 'app-help-tip:not(.closed-tip)' + ); + const focusableContainer = helpTip + ? helpTip + : window.document.querySelector('app-settings'); + + this.focusManagerService.focusFirstElementInContainer(focusableContainer); + }); + } + + private disableSettings(): void { + this.settingsStore.setFormDisable(this.settingForm); + } + + private enableSettings(): void { + this.settingsStore.setFormEnable(this.settingForm); + } + + private createSettingForm() { + this.settingForm = this.fb.group( + { + device_intf: [''], + internet_intf: [''], + log_level: [''], + monitor_period: [''], + }, + { + validators: [this.onlyDifferentValuesValidator.onlyDifferentSetting()], + updateOn: 'change', + } + ); + } + + private createAnalyticsForm() { + this.analyticsForm = this.fb.group({ + optOut: new FormControl(!this.localStorageService.getGAConsent()), + }); + } + + private setDefaultFormValues() { + this.settingsStore.setDefaultFormValues(this.settingForm); + } + + private cleanFormErrorMessage(): void { + this.settingForm.valueChanges + .pipe( + takeUntil(this.destroy$), + tap(() => this.settingsStore.setIsSubmitting(false)) + ) + .subscribe(); + } + + private createSystemConfig(): void { + const { device_intf, internet_intf, log_level, monitor_period } = + this.settingForm.value; + const data: SystemConfig = { + network: { + device_intf: device_intf.key, + internet_intf: this.isInternetControlDisabled ? '' : internet_intf.key, + }, + log_level: log_level.key, + monitor_period: Number(monitor_period.key), + }; + this.settingsStore.updateSystemConfig({ + onSystemConfigUpdate: () => { + this.setDefaultFormValues(); + }, + config: data, + }); + gtag('consent', 'update', { + analytics_storage: this.analyticsForm.value.optOut ? 'denied' : 'granted', + }); + this.localStorageService.setGAConsent(!this.analyticsForm.value.optOut); + } + + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + + getSystemInterfaces(): void { + this.settingsStore.getInterfaces(); + this.hideLoading(); + } + + getSystemConfig(): void { + this.settingsStore.getSystemConfig(); + } + + private showLoading() { + this.loaderService.setLoading(true); + } + + private hideLoading() { + this.loaderService.setLoading(false); + } +} diff --git a/modules/ui/src/app/pages/settings/settings.store.spec.ts b/modules/ui/src/app/pages/general-settings/general-settings.store.spec.ts similarity index 65% rename from modules/ui/src/app/pages/settings/settings.store.spec.ts rename to modules/ui/src/app/pages/general-settings/general-settings.store.spec.ts index 669faef98..562a06ee5 100644 --- a/modules/ui/src/app/pages/settings/settings.store.spec.ts +++ b/modules/ui/src/app/pages/general-settings/general-settings.store.spec.ts @@ -14,59 +14,69 @@ * limitations under the License. */ import { - DEFAULT_INTERNET_OPTION, LOG_LEVELS, MONITORING_PERIOD, - SettingsStore, -} from './settings.store'; + GeneralSettingsStore, +} from './general-settings.store'; import { TestRunService } from '../../services/test-run.service'; import SpyObj = jasmine.SpyObj; import { TestBed } from '@angular/core/testing'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { AppState } from '../../store/state'; import { skip, take } from 'rxjs'; -import { selectHasConnectionSettings } from '../../store/selectors'; +import { + selectAdapters, + selectHasConnectionSettings, + selectInterfaces, + selectSystemConfig, +} from '../../store/selectors'; import { of } from 'rxjs/internal/observable/of'; -import { fetchSystemConfigSuccess } from '../../store/actions'; +import { + fetchInterfaces, + fetchSystemConfig, + fetchSystemConfigSuccess, +} from '../../store/actions'; import { fetchInterfacesSuccess } from '../../store/actions'; import { FormBuilder, FormControl } from '@angular/forms'; -import { FormKey, SystemConfig } from '../../model/setting'; +import { FormKey } from '../../model/setting'; import { + MOCK_ADAPTERS, MOCK_DEVICE_VALUE, MOCK_INTERFACE_VALUE, MOCK_INTERFACES, - MOCK_INTERNET_OPTIONS, MOCK_LOG_VALUE, MOCK_PERIOD_VALUE, MOCK_SYSTEM_CONFIG_WITH_DATA, MOCK_SYSTEM_CONFIG_WITH_NO_DATA, + MOCK_SYSTEM_CONFIG_WITH_SINGLE_PORT, } from '../../mocks/settings.mock'; -describe('SettingsStore', () => { - let settingsStore: SettingsStore; +describe('GeneralSettingsStore', () => { + let settingsStore: GeneralSettingsStore; let mockService: SpyObj; let store: MockStore; let fb: FormBuilder; beforeEach(() => { - mockService = jasmine.createSpyObj([ - 'getSystemInterfaces', - 'createSystemConfig', - 'getSystemConfig', - ]); + mockService = jasmine.createSpyObj(['createSystemConfig']); TestBed.configureTestingModule({ providers: [ - SettingsStore, + GeneralSettingsStore, { provide: TestRunService, useValue: mockService }, provideMockStore({ - selectors: [{ selector: selectHasConnectionSettings, value: true }], + selectors: [ + { selector: selectHasConnectionSettings, value: true }, + { selector: selectAdapters, value: {} }, + { selector: selectInterfaces, value: {} }, + { selector: selectSystemConfig, value: { network: {} } }, + ], }), FormBuilder, ], }); - settingsStore = TestBed.inject(SettingsStore); + settingsStore = TestBed.inject(GeneralSettingsStore); store = TestBed.inject(MockStore); fb = TestBed.inject(FormBuilder); spyOn(store, 'dispatch').and.callFake(() => {}); @@ -77,28 +87,6 @@ describe('SettingsStore', () => { }); describe('updaters', () => { - describe('setSystemConfig', () => { - it('should update systemConfig', (done: DoneFn) => { - const config = { - network: { - device_intf: 'enx207bd2620617', - internet_intf: 'enx207bd2620618', - }, - log_level: 'INFO', - startup_timeout: 60, - monitor_period: 60, - runtime: 120, - } as SystemConfig; - - settingsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.systemConfig).toEqual(config); - done(); - }); - - settingsStore.setSystemConfig(config); - }); - }); - it('should update isSubmitting', (done: DoneFn) => { settingsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { expect(store.isSubmitting).toEqual(true); @@ -109,11 +97,9 @@ describe('SettingsStore', () => { }); it('should update interfaces', (done: DoneFn) => { - settingsStore.viewModel$.pipe(skip(3), take(1)).subscribe(store => { - expect(store.interfaces).toEqual(MOCK_INTERFACES); + settingsStore.viewModel$.pipe(skip(2), take(1)).subscribe(store => { expect(store.deviceOptions).toEqual(MOCK_INTERFACES); expect(store.internetOptions).toEqual({ - '': 'Not specified', mockDeviceKey: 'mockDeviceValue', mockInternetKey: 'mockInternetValue', }); @@ -145,52 +131,18 @@ describe('SettingsStore', () => { describe('effects', () => { describe('getSystemConfig', () => { - beforeEach(() => { - mockService.getSystemConfig.and.returnValue(of({ network: {} })); - }); - - it('should dispatch action fetchSystemConfigSuccess', () => { + it('should dispatch action fetchSystemConfig', () => { settingsStore.getSystemConfig(); - expect(store.dispatch).toHaveBeenCalledWith( - fetchSystemConfigSuccess({ systemConfig: { network: {} } }) - ); - }); - - it('should update store', done => { - settingsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.systemConfig).toEqual({ network: {} }); - done(); - }); - - settingsStore.getSystemConfig(); + expect(store.dispatch).toHaveBeenCalledWith(fetchSystemConfig()); }); }); describe('getInterfaces', () => { - const interfaces = MOCK_INTERFACES; - - beforeEach(() => { - mockService.getSystemInterfaces.and.returnValue(of(interfaces)); - }); - it('should dispatch action fetchInterfacesSuccess', () => { settingsStore.getInterfaces(); - expect(store.dispatch).toHaveBeenCalledWith( - fetchInterfacesSuccess({ interfaces }) - ); - }); - - it('should update store', done => { - settingsStore.viewModel$.pipe(skip(3), take(1)).subscribe(store => { - expect(store.interfaces).toEqual(interfaces); - expect(store.deviceOptions).toEqual(interfaces); - expect(store.internetOptions).toEqual(MOCK_INTERNET_OPTIONS); - done(); - }); - - settingsStore.getInterfaces(); + expect(store.dispatch).toHaveBeenCalledWith(fetchInterfaces()); }); }); @@ -212,20 +164,6 @@ describe('SettingsStore', () => { ); }); - it('should update store', done => { - settingsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.systemConfig).toEqual({ network: {} }); - done(); - }); - - settingsStore.updateSystemConfig( - of({ - onSystemConfigUpdate: () => {}, - config: { network: {} }, - }) - ); - }); - it('should call onSystemConfigUpdate', () => { const effectParams = { onSystemConfigUpdate: () => {}, @@ -244,8 +182,12 @@ describe('SettingsStore', () => { describe('setDefaultFormValues', () => { describe('when values are present', () => { beforeEach(() => { - settingsStore.setSystemConfig(MOCK_SYSTEM_CONFIG_WITH_DATA); - settingsStore.setInterfaces(MOCK_INTERFACES); + store.overrideSelector(selectInterfaces, MOCK_INTERFACES); + store.overrideSelector( + selectSystemConfig, + MOCK_SYSTEM_CONFIG_WITH_DATA + ); + store.refreshState(); }); it('should set default form values', () => { @@ -272,10 +214,39 @@ describe('SettingsStore', () => { }); }); + describe('with single port mode', () => { + beforeEach(() => { + store.overrideSelector(selectInterfaces, MOCK_INTERFACES); + store.overrideSelector( + selectSystemConfig, + MOCK_SYSTEM_CONFIG_WITH_SINGLE_PORT + ); + store.refreshState(); + }); + + it('should disable internet control', () => { + const form = fb.group({ + device_intf: ['value'], + internet_intf: [''], + log_level: [''], + monitor_period: ['value'], + }); + settingsStore.setDefaultFormValues(form); + + expect( + (form.get(FormKey.INTERNET) as FormControl).disabled + ).toBeTrue(); + }); + }); + describe('when values are empty', () => { beforeEach(() => { - settingsStore.setSystemConfig(MOCK_SYSTEM_CONFIG_WITH_NO_DATA); - settingsStore.setInterfaces(MOCK_INTERFACES); + store.overrideSelector(selectInterfaces, MOCK_INTERFACES); + store.overrideSelector( + selectSystemConfig, + MOCK_SYSTEM_CONFIG_WITH_NO_DATA + ); + store.refreshState(); }); it('should set default form values', () => { @@ -293,7 +264,7 @@ describe('SettingsStore', () => { }); expect((form.get(FormKey.INTERNET) as FormControl).value).toEqual({ key: '', - value: DEFAULT_INTERNET_OPTION[''], + value: undefined, }); expect((form.get(FormKey.LOG_LEVEL) as FormControl).value).toEqual({ key: 'INFO', @@ -308,5 +279,39 @@ describe('SettingsStore', () => { }); }); }); + + describe('adaptersUpdate', () => { + const updateInterfaces = { + mockDeviceKey: 'mockDeviceValue', + mockNewInternetKey: 'mockNewInternetValue', + }; + const updateInternetOptions = { + mockDeviceKey: 'mockDeviceValue', + mockNewInternetKey: 'mockNewInternetValue', + }; + + beforeEach(() => { + settingsStore.setInterfaces(MOCK_INTERFACES); + store.overrideSelector(selectInterfaces, MOCK_INTERFACES); + store.refreshState(); + }); + + it('should update store', done => { + settingsStore.viewModel$ + .pipe(skip(2), take(1)) + .subscribe(storeValue => { + expect(storeValue.deviceOptions).toEqual(updateInterfaces); + expect(storeValue.internetOptions).toEqual(updateInternetOptions); + + expect(store.dispatch).toHaveBeenCalledWith( + fetchInterfacesSuccess({ interfaces: updateInterfaces }) + ); + done(); + }); + + store.overrideSelector(selectAdapters, MOCK_ADAPTERS); + store.refreshState(); + }); + }); }); }); diff --git a/modules/ui/src/app/pages/settings/settings.store.ts b/modules/ui/src/app/pages/general-settings/general-settings.store.ts similarity index 65% rename from modules/ui/src/app/pages/settings/settings.store.ts rename to modules/ui/src/app/pages/general-settings/general-settings.store.ts index f489228a9..ec2b5c99f 100644 --- a/modules/ui/src/app/pages/settings/settings.store.ts +++ b/modules/ui/src/app/pages/general-settings/general-settings.store.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { ComponentStore } from '@ngrx/component-store'; import { TestRunService } from '../../services/test-run.service'; import { @@ -23,30 +23,31 @@ import { SystemConfig, SystemInterfaces, } from '../../model/setting'; -import { exhaustMap, switchMap, Observable } from 'rxjs'; +import { exhaustMap, switchMap, Observable, skip } from 'rxjs'; import { tap, withLatestFrom } from 'rxjs/operators'; import * as AppActions from '../../store/actions'; import { Store } from '@ngrx/store'; import { AppState } from '../../store/state'; -import { selectHasConnectionSettings } from '../../store/selectors'; +import { + selectAdapters, + selectHasConnectionSettings, + selectInterfaces, + selectSystemConfig, + selectSystemStatus, +} from '../../store/selectors'; import { FormControl, FormGroup } from '@angular/forms'; +import { fetchInterfaces, fetchSystemConfig } from '../../store/actions'; export interface SettingsComponentState { hasConnectionSettings: boolean; isSubmitting: boolean; - systemConfig: SystemConfig; isLessThanOneInterface: boolean; - interfaces: SystemInterfaces; deviceOptions: SystemInterfaces; internetOptions: SystemInterfaces; logLevelOptions: { [key: string]: string }; monitoringPeriodOptions: SystemInterfaces; } -export const DEFAULT_INTERNET_OPTION = { - '': 'Not specified', -}; - export const LOG_LEVELS = { DEBUG: 'Every event will be logged', INFO: 'Normal events and issues', @@ -68,18 +69,26 @@ export const MONITORING_PERIOD = { 600: 'Very slow device', }; @Injectable() -export class SettingsStore extends ComponentStore { +export class GeneralSettingsStore extends ComponentStore { + private testRunService = inject(TestRunService); + private store = inject>(Store); + private static readonly DEFAULT_LOG_LEVEL = 'INFO'; private static readonly DEFAULT_MONITORING_PERIOD = '300'; - private systemConfig$ = this.select(state => state.systemConfig); private hasConnectionSettings$ = this.store.select( selectHasConnectionSettings ); + + private adapters$ = this.store.select(selectAdapters); + systemStatus$ = this.store.select(selectSystemStatus); private isSubmitting$ = this.select(state => state.isSubmitting); private isLessThanOneInterfaces$ = this.select( state => state.isLessThanOneInterface ); - private interfaces$ = this.select(state => state.interfaces); + private interfaces$: Observable = + this.store.select(selectInterfaces); + private systemConfig$: Observable = + this.store.select(selectSystemConfig); private deviceOptions$ = this.select(state => state.deviceOptions); private internetOptions$ = this.select(state => state.internetOptions); private logLevelOptions$ = this.select(state => state.logLevelOptions); @@ -98,53 +107,41 @@ export class SettingsStore extends ComponentStore { monitoringPeriodOptions: this.monitoringPeriodOptions$, }); - setSystemConfig = this.updater((state, systemConfig: SystemConfig) => ({ - ...state, - systemConfig, - })); - setIsSubmitting = this.updater((state, isSubmitting: boolean) => ({ ...state, isSubmitting, })); - setInterfaces = this.updater((state, interfaces: SystemInterfaces) => ({ - ...state, - interfaces, - deviceOptions: interfaces, - internetOptions: { - ...DEFAULT_INTERNET_OPTION, - ...interfaces, - }, - isLessThanOneInterface: Object.keys(interfaces).length < 1, - })); + setInterfaces = this.updater((state, interfaces: SystemInterfaces) => { + return { + ...state, + deviceOptions: interfaces, + internetOptions: interfaces, + isLessThanOneInterface: Object.keys(interfaces).length < 1, + }; + }); + + statusLoaded = this.effect(() => { + return this.interfaces$.pipe( + skip(1), + tap(interfaces => { + this.setInterfaces(interfaces); + }) + ); + }); getInterfaces = this.effect(trigger$ => { return trigger$.pipe( - exhaustMap(() => { - return this.testRunService.getSystemInterfaces().pipe( - tap((interfaces: SystemInterfaces) => { - this.store.dispatch( - AppActions.fetchInterfacesSuccess({ interfaces }) - ); - this.setInterfaces(interfaces); - }) - ); + tap(() => { + this.store.dispatch(fetchInterfaces()); }) ); }); getSystemConfig = this.effect(trigger$ => { return trigger$.pipe( - exhaustMap(() => { - return this.testRunService.getSystemConfig().pipe( - tap((systemConfig: SystemConfig) => { - this.store.dispatch( - AppActions.fetchSystemConfigSuccess({ systemConfig }) - ); - this.setSystemConfig(systemConfig); - }) - ); + tap(() => { + this.store.dispatch(fetchSystemConfig()); }) ); }); @@ -162,7 +159,6 @@ export class SettingsStore extends ComponentStore { systemConfig: trigger.config, }) ); - this.setSystemConfig(trigger.config); trigger.onSystemConfigUpdate(); }) ); @@ -170,22 +166,47 @@ export class SettingsStore extends ComponentStore { ); }); + setFormDisable = this.effect((formGroup$: Observable) => { + return formGroup$.pipe( + tap(formGroup => { + formGroup.disable(); + }) + ); + }); + + setFormEnable = this.effect((formGroup$: Observable) => { + return formGroup$.pipe( + withLatestFrom(this.systemConfig$), + tap(([formGroup, config]) => { + formGroup.enable(); + if (config.single_intf) { + this.disableInternetInterface(formGroup); + } + }) + ); + }); + setDefaultFormValues = this.effect((formGroup$: Observable) => { return formGroup$.pipe( switchMap(formGroup => this.systemConfig$.pipe( withLatestFrom(this.deviceOptions$, this.internetOptions$), tap(([config, deviceOptions, internetOptions]) => { + if (config.single_intf) { + this.disableInternetInterface(formGroup); + } else { + this.setDefaultInternetInterfaceValue( + config.network?.internet_intf, + internetOptions, + formGroup + ); + } + this.setDefaultDeviceInterfaceValue( config.network?.device_intf, deviceOptions, formGroup ); - this.setDefaultInternetInterfaceValue( - config.network?.internet_intf, - internetOptions, - formGroup - ); this.setDefaultLogLevelValue( config.log_level, LOG_LEVELS, @@ -202,6 +223,48 @@ export class SettingsStore extends ComponentStore { ); }); + adaptersUpdate = this.effect(() => { + return this.adapters$.pipe( + skip(1), + withLatestFrom(this.interfaces$), + tap(([adapters, interfaces]) => { + const updatedInterfaces = { ...interfaces }; + if (adapters.adapters_added) { + this.addInterfaces(adapters.adapters_added, updatedInterfaces); + } + if (adapters.adapters_removed) { + this.removeInterfaces(adapters.adapters_removed, updatedInterfaces); + } + this.updateInterfaces(updatedInterfaces); + }) + ); + }); + + private updateInterfaces(interfaces: SystemInterfaces) { + this.store.dispatch( + AppActions.fetchInterfacesSuccess({ interfaces: interfaces }) + ); + this.setInterfaces(interfaces); + } + + private addInterfaces( + newInterfaces: SystemInterfaces, + interfaces: SystemInterfaces + ): void { + for (const [key, value] of Object.entries(newInterfaces)) { + interfaces[key] = value; + } + } + + private removeInterfaces( + interfacesToDelete: SystemInterfaces, + interfaces: SystemInterfaces + ): void { + for (const key of Object.keys(interfacesToDelete)) { + delete interfaces[key]; + } + } + private setDefaultDeviceInterfaceValue( value: string | undefined, options: { [key: string]: string }, @@ -235,7 +298,7 @@ export class SettingsStore extends ComponentStore { ): void { this.setDefaultValue( value, - SettingsStore.DEFAULT_LOG_LEVEL, + GeneralSettingsStore.DEFAULT_LOG_LEVEL, options, formGroup.get(FormKey.LOG_LEVEL) as FormControl ); @@ -248,12 +311,17 @@ export class SettingsStore extends ComponentStore { ): void { this.setDefaultValue( value, - SettingsStore.DEFAULT_MONITORING_PERIOD, + GeneralSettingsStore.DEFAULT_MONITORING_PERIOD, options, formGroup.get(FormKey.MONITOR_PERIOD) as FormControl ); } + private disableInternetInterface(formGroup: FormGroup) { + const internetControl = formGroup.get(FormKey.INTERNET) as FormControl; + internetControl.disable(); + } + private setDefaultValue( value: string | undefined, defaultValue: string | undefined, @@ -279,16 +347,11 @@ export class SettingsStore extends ComponentStore { }; } - constructor( - private testRunService: TestRunService, - private store: Store - ) { + constructor() { super({ - systemConfig: { network: {} }, hasConnectionSettings: false, isSubmitting: false, isLessThanOneInterface: false, - interfaces: {}, deviceOptions: {}, internetOptions: {}, logLevelOptions: LOG_LEVELS, diff --git a/modules/ui/src/app/pages/settings/only-different-values.validator.ts b/modules/ui/src/app/pages/general-settings/only-different-values.validator.ts similarity index 90% rename from modules/ui/src/app/pages/settings/only-different-values.validator.ts rename to modules/ui/src/app/pages/general-settings/only-different-values.validator.ts index a153fb0ef..735272742 100644 --- a/modules/ui/src/app/pages/settings/only-different-values.validator.ts +++ b/modules/ui/src/app/pages/general-settings/only-different-values.validator.ts @@ -40,7 +40,11 @@ export class OnlyDifferentValuesValidator { return null; } - if (deviceControlValue.key === internetControlValue.key) { + if ( + deviceControlValue.key === internetControlValue.key && + deviceControlValue.key && + internetControlValue.key + ) { return { hasSameValues: true }; } return null; diff --git a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.html b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.html index 7b074ec54..1862f1473 100644 --- a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.html +++ b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.html @@ -18,7 +18,10 @@ class="delete-report-button" href="#" matTooltip="Delete report for Testrun # {{ getTestRunId(data) }}" - (click)="deleteReport($event)"> + [attr.aria-label]="'Delete report for Testrun # {{ getTestRunId(data) }}'" + (click)="deleteReport($event)" + (keydown.enter)="deleteReport($event)" + (keydown.space)="deleteReport($event)"> diff --git a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.scss b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.scss index 487186787..eac5d91fa 100644 --- a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.scss +++ b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.scss @@ -13,19 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@use 'mixins'; + :host { display: inline-block; } .delete-report-button { - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - padding: 4px 0; - margin: 0 4px; - & ::ng-deep .mdc-icon-button__ripple { - display: none; - } + @include mixins.report-action; } diff --git a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.spec.ts b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.spec.ts index f85babe34..46dae86c7 100644 --- a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.spec.ts +++ b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.spec.ts @@ -51,13 +51,27 @@ describe('DeleteReportComponent', () => { it('#deleteReport should open delete dialog', () => { const deviceRemovedSpy = spyOn(component.removeDevice, 'emit'); - spyOn(component.dialog, 'open').and.returnValue({ + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), } as MatDialogRef); component.deleteReport(new Event('click')); expect(deviceRemovedSpy).toHaveBeenCalled(); + + expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, { + ariaLabel: 'Delete report', + data: { + title: 'Delete report?', + content: + 'You are about to delete Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20. Are you sure?', + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: ['simple-dialog', 'delete-dialog'], + }); + openSpy.calls.reset(); }); }); diff --git a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.ts b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.ts index 74abb27d1..3571a8fc4 100644 --- a/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.ts +++ b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.ts @@ -19,6 +19,7 @@ import { EventEmitter, OnDestroy, Output, + inject, } from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; @@ -31,7 +32,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; @Component({ selector: 'app-delete-report', - standalone: true, + imports: [CommonModule, MatButtonModule, MatTooltipModule], templateUrl: './delete-report.component.html', styleUrls: ['./delete-report.component.scss'], @@ -42,13 +43,12 @@ export class DeleteReportComponent extends ReportActionComponent implements OnDestroy { + dialog = inject(MatDialog); + @Output() removeDevice = new EventEmitter(); private destroy$: Subject = new Subject(); - constructor( - public dialog: MatDialog, - datePipe: DatePipe - ) { - super(datePipe); + constructor() { + super(); } ngOnDestroy() { @@ -62,12 +62,12 @@ export class DeleteReportComponent ariaLabel: 'Delete report', data: { title: 'Delete report?', - content: this.getTestRunId(this.data), + content: `You are about to delete ${this.getTestRunId(this.data)}. Are you sure?`, }, autoFocus: true, hasBackdrop: true, disableClose: true, - panelClass: 'simple-dialog', + panelClass: ['simple-dialog', 'delete-dialog'], }); dialogRef diff --git a/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.html b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.html index 07cf43aa0..e7eed0296 100644 --- a/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.html +++ b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.html @@ -39,16 +39,15 @@ - - Clear all filters - +
+
Clear all filters
+
diff --git a/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.scss b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.scss index ec7ef97e0..541540fd5 100644 --- a/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.scss +++ b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.scss @@ -13,45 +13,77 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import 'src/theming/colors'; +@use 'colors'; :host { display: flex; } -.clear-all { - display: flex; - align-items: center; - justify-content: center; - padding: 0 8px; - cursor: pointer; - color: $primary; - font-weight: 400; - height: 32px; - margin: 4px 0 4px 8px; - border-radius: 16px; - flex-shrink: 0; - background: $white; - font-family: Roboto, sans-serif; -} - .filter-chip.mat-mdc-chip { - background: $white; - border: 1px solid $primary; + background: colors.$light-grey; + &::ng-deep .mat-mdc-chip-primary-focus-indicator { + display: none; + } } .filter-chip.mat-mdc-chip ::ng-deep .mat-mdc-chip-action-label { - color: $primary; + color: colors.$on-secondary-container; font-weight: 500; } .filter-chip.mat-mdc-chip ::ng-deep .mat-mdc-chip-remove { - color: $primary; + color: colors.$on-secondary-container; } .filter-chip.cdk-keyboard-focused, .filter-chip-remove:focus-visible { - outline: $black solid 2px; + outline: colors.$black solid 2px; + &:before { + content: none; + } +} + +.filter-chip-remove:focus { + &:before { + content: none; + } +} + +.clear-button:has(.clear-button-label:focus), +.clear-button.cdk-keyboard-focused { + outline: colors.$black solid 2px; +} + +.clear-button-label:focus { + border: none; +} + +.clear-button { + height: var(--mdc-text-button-container-height); + border-radius: var(--mdc-text-button-container-shape); + margin: 0 0 0 8px !important; + border: none; + display: flex; + align-items: center; + justify-content: center; +} + +.clear-button .clear-button-label { + color: colors.$primary !important; + border: none; + outline: none; +} + +.clear-button ::ng-deep .mat-focus-indicator { + display: none; +} + +.clear-button + ::ng-deep + .mdc-evolution-chip__action--primary:not( + .mdc-evolution-chip__action--presentational + ):not(.mdc-ripple-upgraded):focus::before { + border: none; } .filter-chip .filter-chip-remove { @@ -64,7 +96,7 @@ .mat-mdc-standard-chip:not( .mdc-evolution-chip--disabled ).filter-chip-clear-all { - background-color: $white; + background-color: colors.$white; & ::ng-deep .mat-mdc-chip-action { padding: 0; diff --git a/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.spec.ts b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.spec.ts index 34cb9459f..ec48d528a 100644 --- a/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.spec.ts +++ b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.spec.ts @@ -36,10 +36,44 @@ describe('FilterChipsComponent', () => { expect(component).toBeTruthy(); }); + describe('#clearFilter', () => { + const MOCK_FILTERS = { + deviceInfo: 'Delta', + deviceFirmware: '03', + results: ['Compliant'], + dateRange: { start: '10/2/2024', end: '11/2/2024' }, + }; + + beforeEach(() => { + component.filters = MOCK_FILTERS; + }); + + it(`should clear deviceFirmware filter`, () => { + const result = { ...MOCK_FILTERS, deviceFirmware: '' }; + component.clearFilter('deviceFirmware'); + + expect(component.filters).toEqual(result); + }); + + it(`should clear results filter`, () => { + const clearedFilters = { ...MOCK_FILTERS, results: [] }; + component.clearFilter('results'); + + expect(component.filters).toEqual(clearedFilters); + }); + + it(`should clear dateRange filter`, () => { + const clearedFilters = { ...MOCK_FILTERS, dateRange: '' }; + component.clearFilter('dateRange'); + + expect(component.filters).toEqual(clearedFilters); + }); + }); + describe('DOM tests', () => { describe('"Clear all filters" button', () => { it('should exist', () => { - const button = compiled.querySelector('.clear-all'); + const button = compiled.querySelector('.clear-button'); expect(button).toBeTruthy(); }); @@ -47,7 +81,7 @@ describe('FilterChipsComponent', () => { it('should clear all filters on click', () => { const clearAllFiltersSpy = spyOn(component, 'clearAllFilters'); const button = compiled.querySelector( - '.clear-all' + '.clear-button' ) as HTMLButtonElement; button.click(); diff --git a/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.ts b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.ts index 45e4cce62..f2d64a93d 100644 --- a/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.ts +++ b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.ts @@ -18,13 +18,20 @@ import { MatIconModule } from '@angular/material/icon'; import { MatChipsModule } from '@angular/material/chips'; import { CommonModule, KeyValuePipe } from '@angular/common'; import { DateRange, FilterName, Filters } from '../../../../model/filters'; +import { MatButtonModule } from '@angular/material/button'; @Component({ selector: 'app-filter-chips', templateUrl: './filter-chips.component.html', styleUrls: ['./filter-chips.component.scss'], - standalone: true, - imports: [MatIconModule, MatChipsModule, KeyValuePipe, CommonModule], + + imports: [ + MatIconModule, + MatChipsModule, + MatButtonModule, + KeyValuePipe, + CommonModule, + ], }) export class FilterChipsComponent { @Input() filters!: Filters; diff --git a/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.html b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.html index 5f936e1ce..17c4a7915 100644 --- a/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.html +++ b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.html @@ -13,10 +13,14 @@ See the License for the specific language governing permissions and limitations under the License. --> +

{{ data.title }}

+ -
+ [ngStyle]="{ + 'max-height': 'calc(100vh - ' + (topPosition + dialogTitle) + 'px)', + }"> + - Please enter device model name - Please enter firmware name -
+

@@ -73,30 +78,28 @@ - Started date + Dates - MM/DD/YYYY – MM/DD/YYYY - Please, select the correct date range in MM/DD/YYYY format. + Please, select the correct date range in mm/dd/yyyy format. @@ -109,8 +112,6 @@ - - + + diff --git a/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.scss b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.scss index 6b65c0a82..a6da7dd55 100644 --- a/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.scss +++ b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.scss @@ -14,12 +14,35 @@ * limitations under the License. */ @use 'node_modules/@angular/material/index' as mat; -@import 'src/theming/colors'; +@use 'm3-theme' as *; +@use 'colors'; +@use 'variables'; + +.filter-dialog-title { + padding: 24px 12px 16px 24px; + display: flex; + border-bottom: 1px solid colors.$outline-variant; + &:before { + height: 0; + } +} .filter-dialog-content { display: flex; flex-direction: column; - padding: 16px 16px 0 16px; + padding: 0 4px; + + &:has(.text-field) { + padding: 0 24px 16px; + } + + &:has(.filter-result-item) { + padding: 0 16px; + } +} + +.filter-form { + padding-top: 16px; } .date-field { @@ -28,51 +51,100 @@ .date-calendar { flex-grow: 1; - overflow: auto; min-height: 2em; + + &::ng-deep .mat-calendar-body-label { + visibility: hidden; + } + &::ng-deep .mat-calendar-body-label[colspan='7'] { + display: none; + } + + &::ng-deep mat-year-view .mat-calendar-body-label[colspan='4'] { + display: none; + } + + &::ng-deep .mat-calendar-header { + padding-top: 0; + } + + ::ng-deep .mat-calendar-header button .mat-focus-indicator { + display: none; + } + + ::ng-deep.mat-calendar-body-cell:focus .mat-focus-indicator::before { + content: none; + } + + ::ng-deep.mat-calendar-body-cell:focus-visible .mat-focus-indicator::before { + content: ''; + } } .filter-dialog-actions { - border-top: 1px solid $lighter-grey; - gap: 16px; + padding: 8px 12px 24px; + font-family: variables.$font-text; + gap: 8px; button { min-width: 38px; margin: 0; - padding: 0 8px; - color: mat.get-color-from-palette($color-primary, 600); + padding: 0 16px; font-weight: 500; line-height: 20px; - letter-spacing: 0.25px; + + ::ng-deep .mat-focus-indicator { + display: none; + } } } -.text-field, -.date-field { +.text-field { width: 100%; } +.date-field { + margin: 0 12px; +} + +.filter-result { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 16px; + + ::ng-deep .mdc-checkbox__native-control:focus ~ .mat-focus-indicator::before { + content: none; + } + + ::ng-deep + .mdc-checkbox__native-control:focus-visible + ~ .mat-focus-indicator::before { + content: ''; + } +} + .filter-result-item { - padding: 4px 0; + padding: 8px 0; margin: 0; +} - &:first-child { - padding-top: 0; +.date-field, +.text-field { + &::ng-deep.mat-mdc-form-field-subscript-wrapper { + display: none; } - &:last-child { - padding-bottom: 20px; + &::ng-deep.mat-mdc-form-field-subscript-wrapper:has(mat-error) { + display: block; } } .text-field { - padding-bottom: 10px; - &::ng-deep.mat-mdc-form-field-subscript-wrapper:has(mat-error) { height: 60px; } - &::ng-deep.mat-mdc-form-field-hint-wrapper, &::ng-deep.mat-mdc-form-field-error-wrapper { padding: 0 10px; } diff --git a/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.spec.ts b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.spec.ts index dfa48e8f0..d866a1f37 100644 --- a/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.spec.ts +++ b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.spec.ts @@ -33,7 +33,7 @@ import { MatDatepickerModule, } from '@angular/material/datepicker'; import { MatNativeDateModule } from '@angular/material/core'; -import { DateRange, FilterName } from '../../../../model/filters'; +import { DateRange, FilterName, FilterTitle } from '../../../../model/filters'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { of } from 'rxjs'; @@ -84,6 +84,7 @@ describe('FilterDialogComponent', () => { component.data = { trigger: mockClientRest, filter: FilterName.DeviceInfo, + title: FilterTitle.DeviceInfo, }; component.data.trigger.nativeElement = { getBoundingClientRect: () => mockData, @@ -152,6 +153,7 @@ describe('FilterDialogComponent', () => { component.data = { trigger: mockClientRest, filter: FilterName.DeviceFirmware, + title: FilterTitle.DeviceFirmware, }; fixture.detectChanges(); @@ -178,6 +180,7 @@ describe('FilterDialogComponent', () => { component.data = { trigger: mockClientRest, filter: FilterName.Started, + title: FilterTitle.Started, }; fixture.detectChanges(); }); @@ -246,7 +249,9 @@ describe('FilterDialogComponent', () => { }); it('should max date as today', () => { - expect(component.calendar.maxDate?.getDate()).toBe(new Date().getDate()); + expect(component.calendar()?.maxDate?.getDate()).toBe( + new Date().getDate() + ); }); }); }); diff --git a/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.ts b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.ts index 3d8ac231a..26bce3260 100644 --- a/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.ts +++ b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.ts @@ -18,9 +18,9 @@ import { Component, ElementRef, HostListener, - Inject, OnInit, - ViewChild, + viewChild, + inject, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { @@ -36,7 +36,9 @@ import { FormBuilder, FormControl, FormGroup, + FormGroupDirective, FormsModule, + NgForm, NgModel, ReactiveFormsModule, } from '@angular/forms'; @@ -52,23 +54,43 @@ import { MatDatepickerInputEvent, MatDatepickerModule, } from '@angular/material/datepicker'; -import { MatNativeDateModule } from '@angular/material/core'; +import { ErrorStateMatcher, MatNativeDateModule } from '@angular/material/core'; import { FilterName, DateRange as LocalDateRange, } from '../../../../model/filters'; import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; -import { StatusOfTestResult } from '../../../../model/testrun-status'; +import { + ResultOfTestrun, + StatusOfTestrun, +} from '../../../../model/testrun-status'; import { DeviceValidators } from '../../../devices/components/device-form/device.validators'; +class DateErrorStateMatcher implements ErrorStateMatcher { + isErrorState( + control: FormControl | null, + form: FormGroupDirective | NgForm | null + ): boolean { + const isSubmitted = form && form.submitted; + return !!( + control && + control.errors && + control.errors['matDatepickerParse'].text.trim().length > 0 && + control.invalid && + (control.touched || isSubmitted) + ); + } +} + interface DialogData { trigger: ElementRef; filter: string; + title: string; } @Component({ selector: 'app-filter-dialog', - standalone: true, + imports: [ CommonModule, MatDialogModule, @@ -96,9 +118,16 @@ export class FilterDialogComponent extends EscapableDialogComponent implements OnInit { + override dialogRef: MatDialogRef; + private deviceValidators = inject(DeviceValidators); + data = inject(MAT_DIALOG_DATA); + private fb = inject(FormBuilder); + resultList = [ - { value: StatusOfTestResult.Compliant, enabled: false }, - { value: StatusOfTestResult.NonCompliant, enabled: false }, + { value: ResultOfTestrun.Compliant, enabled: false }, + { value: ResultOfTestrun.NonCompliant, enabled: false }, + { value: StatusOfTestrun.Proceed, enabled: false }, + { value: StatusOfTestrun.DoNotProceed, enabled: false }, ]; filterForm!: FormGroup; selectedRangeValue!: DateRange | undefined; @@ -108,6 +137,7 @@ export class FilterDialogComponent range: LocalDateRange = new LocalDateRange(); topPosition = 0; + dialogTitle = 110; today = new Date(); @@ -118,17 +148,15 @@ export class FilterDialogComponent this.setDialogView(); } - @ViewChild(MatCalendar) calendar!: MatCalendar; - @ViewChild('startDate') startDate!: NgModel; - @ViewChild('endDate') endDate!: NgModel; - - constructor( - public override dialogRef: MatDialogRef, - private deviceValidators: DeviceValidators, - @Inject(MAT_DIALOG_DATA) public data: DialogData, - private fb: FormBuilder - ) { - super(dialogRef); + readonly calendar = viewChild(MatCalendar); + readonly startDate = viewChild('startDate'); + readonly endDate = viewChild('endDate'); + + constructor() { + const dialogRef = inject>(MatDialogRef); + + super(); + this.dialogRef = dialogRef; } get deviceInfo() { @@ -149,12 +177,21 @@ export class FilterDialogComponent const rect = this.data.trigger?.nativeElement.getBoundingClientRect(); matDialogConfig.position = { - left: `${rect.left - 80}px`, - top: `${rect.bottom + 0}px`, + left: + this.data.filter === FilterName.Results + ? `${rect.left - 240}px` + : `${rect.left}px`, + top: `${rect.bottom + 14}px`, }; this.topPosition = rect.bottom + this.dialog_actions_height; - matDialogConfig.width = this.data.filter === 'results' ? '240px' : '328px'; + if (this.data.filter === FilterName.Started) { + matDialogConfig.width = '360px'; + } else if (this.data.filter === FilterName.Results) { + matDialogConfig.width = '240px'; + } else { + matDialogConfig.width = '328px'; + } this.dialogRef.updateSize(matDialogConfig.width); this.dialogRef.updatePosition(matDialogConfig.position); @@ -191,8 +228,8 @@ export class FilterDialogComponent confirm(): void { if ( this.filterForm?.invalid || - this.startDate?.invalid || - this.endDate?.invalid + this.startDate()?.invalid || + this.endDate()?.invalid ) { return; } @@ -225,6 +262,8 @@ export class FilterDialogComponent this.dialogRef.close(); } + dateMatcher = new DateErrorStateMatcher(); + startDateChanged(event: MatDatepickerInputEvent) { const date = event.value; if (date && date.getFullYear() > this.today.getFullYear()) { @@ -236,8 +275,11 @@ export class FilterDialogComponent this.selectedRangeValue?.end || null ); if (this.selectedRangeValue.start) { - this.calendar.activeDate = this.selectedRangeValue.start; - this.calendar.updateTodaysDate(); + const calendar = this.calendar(); + if (calendar) { + calendar.activeDate = this.selectedRangeValue.start; + calendar.updateTodaysDate(); + } } } @@ -252,8 +294,11 @@ export class FilterDialogComponent event.value ); if (this.selectedRangeValue?.end) { - this.calendar.activeDate = this.selectedRangeValue.end; - this.calendar.updateTodaysDate(); + const calendar = this.calendar(); + if (calendar) { + calendar.activeDate = this.selectedRangeValue.end; + calendar.updateTodaysDate(); + } } } } diff --git a/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.html b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.html new file mode 100644 index 000000000..21f94113d --- /dev/null +++ b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.html @@ -0,0 +1,38 @@ + + {{ headerText }} + + + + + + + diff --git a/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.scss b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.scss new file mode 100644 index 000000000..a965cb6b1 --- /dev/null +++ b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.scss @@ -0,0 +1,43 @@ +@use 'node_modules/@angular/material/index' as mat; +@use 'src/theming/m3-theme' as *; +@use 'colors'; +@use 'variables'; + +:host { + display: contents; +} + +th { + height: var(--mat-table-header-container-height); + vertical-align: middle; +} + +.filter-button { + display: flex; + width: variables.$reports-table-header-size; + height: variables.$reports-table-header-size; + justify-content: center; + align-items: center; + flex-shrink: 0; + margin-left: 8px; + padding: 0; + border: none; + background: colors.$white; + cursor: pointer; + border-radius: 50%; + &:hover { + background-color: rgba(0, 0, 0, 0.04); + } + &:active, + &.active { + background-color: colors.$secondary-container; + color: colors.$on-secondary-container; + &:hover { + filter: brightness(90%); + } + } +} + +.filter-button.active .mat-icon { + color: mat.get-theme-color($light-theme, primary, 35); +} diff --git a/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.spec.ts b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.spec.ts new file mode 100644 index 000000000..d530772d3 --- /dev/null +++ b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.spec.ts @@ -0,0 +1,94 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FilterHeaderComponent } from './filter-header.component'; +import { MatIconModule } from '@angular/material/icon'; +import { MatSortModule } from '@angular/material/sort'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTableModule } from '@angular/material/table'; +import { CommonModule } from '@angular/common'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +@Component({ + selector: 'app-dummy-table', + template: ` + + + + + + + + +
{{ element }}
+ `, + standalone: false, +}) +export class DummyTableComponent { + data = ['Row 1', 'Row 2', 'Row 3']; + displayedColumns = ['testColumn']; +} + +describe('FilterHeaderComponent within mat-table', () => { + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DummyTableComponent], + imports: [ + BrowserAnimationsModule, + FilterHeaderComponent, + MatIconModule, + MatSortModule, + MatButtonModule, + MatTableModule, + CommonModule, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DummyTableComponent); + fixture.detectChanges(); + compiled = fixture.nativeElement as HTMLElement; + }); + + it('should have the filter header component', () => { + const filterHeader = compiled.querySelector( + 'app-filter-header' + ) as HTMLElement; + expect(filterHeader).toBeTruthy(); + const headerText = filterHeader?.querySelector( + 'th span' + ) as HTMLSpanElement; + expect(headerText?.textContent?.trim()).toBe('Test Header'); + }); + + it('should emit an event when filter button is clicked in filter header', () => { + const filterHeader = fixture.debugElement.query( + By.css('app-filter-header') + ); + const filterHeaderComponent = + filterHeader.componentInstance as FilterHeaderComponent; + + spyOn(filterHeaderComponent.emitOpenFilter, 'emit'); + + const button = filterHeader.query(By.css('.filter-button')) + .nativeElement as HTMLButtonElement; + button.click(); + + expect(filterHeaderComponent.emitOpenFilter.emit).toHaveBeenCalledWith({ + event: new PointerEvent('event'), + filter: 'testFilter', + title: 'testTitle', + filterOpened: false, + }); + }); +}); diff --git a/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.ts b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.ts new file mode 100644 index 000000000..c261a8347 --- /dev/null +++ b/modules/ui/src/app/pages/reports/components/filter-header/filter-header.component.ts @@ -0,0 +1,51 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule } from '@angular/common'; +import { MatTableModule } from '@angular/material/table'; +import { MatSortModule } from '@angular/material/sort'; + +export interface OpenFilterEvent { + event: Event; + filter: string; + title: string; + filterOpened: boolean; +} + +@Component({ + selector: 'app-filter-header', + imports: [ + MatIconModule, + MatButtonModule, + CommonModule, + MatTableModule, + MatSortModule, + ], + templateUrl: './filter-header.component.html', + styleUrl: './filter-header.component.scss', +}) +export class FilterHeaderComponent { + @Output() emitOpenFilter = new EventEmitter(); + @Input({ required: true }) filterName!: string; + @Input({ required: true }) filterTitle!: string; + @Input({ required: true }) filterOpened!: boolean; + @Input() hasSorting: boolean = true; + @Input() filtered: boolean = false; + @Input({ required: true }) activeFilter!: string; + @Input() sortActionDescription: string = ''; + @Input({ required: true }) headerText!: string; + + openFilter( + event: Event, + filter: string, + title: string, + filterOpened: boolean + ): void { + this.emitOpenFilter.emit({ + event, + filter, + title, + filterOpened, + }); + } +} diff --git a/modules/ui/src/app/pages/reports/reports.component.html b/modules/ui/src/app/pages/reports/reports.component.html index 6fc7017a8..ee4b10a10 100644 --- a/modules/ui/src/app/pages/reports/reports.component.html +++ b/modules/ui/src/app/pages/reports/reports.component.html @@ -15,134 +15,129 @@ --> - - -

Reports

- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + +
- Started - - - - {{ getFormattedDateString(data.started) }} - - Duration - - {{ data.duration }} - - Device - - - {{ data.deviceInfo }} - Firmware - - - {{ data.deviceFirmware }} - Result - - - - - {{ data.status }} - - Actions + +

Reports

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - -
+ {{ getFormattedDateString(data.started) }} + + Duration + + {{ data.duration }} + {{ data.deviceInfo }}{{ data.deviceFirmware }} + Assessment type + + {{ data.program }} + + + {{ data.testResult }} + + Actions +
Reports Reports delete -
-
- -
-
-
- -
-
-
- + +
+
+ +
+
+
+ +
+
+
- - -
- -
-
- - - - - -
-
- {{ header }} - {{ message }} -
+ +
diff --git a/modules/ui/src/app/pages/reports/reports.component.scss b/modules/ui/src/app/pages/reports/reports.component.scss index 47d76d880..59050176f 100644 --- a/modules/ui/src/app/pages/reports/reports.component.scss +++ b/modules/ui/src/app/pages/reports/reports.component.scss @@ -14,8 +14,10 @@ * limitations under the License. */ @use 'node_modules/@angular/material/index' as mat; -@import 'src/theming/colors'; -@import 'src/theming/variables'; +@use 'm3-theme' as *; +@use 'colors'; +@use 'mixins'; +@use 'variables'; :host { overflow: hidden; @@ -25,7 +27,7 @@ .history-toolbar { gap: 10px; - background: $white; + background: colors.$white; height: 74px; padding: 24px 0 8px 32px; } @@ -33,8 +35,6 @@ .history-content { margin: 10px 32px 39px 32px; overflow-y: auto; - border-radius: 4px; - border: 1px solid $lighter-grey; height: -webkit-max-content; height: -moz-max-content; height: max-content; @@ -52,20 +52,16 @@ } .history-content table { - th { - font-weight: 700; - } - - td { - font-weight: 400; - } - - th, - td { - font-family: Roboto, sans-serif; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.2px; + --mat-table-header-headline-font: #{variables.$font-text}; + --mat-table-row-item-label-text-font: #{variables.$font-text}; + --mat-table-background-color: #{colors.$surface}; + --mat-table-header-headline-color: #{colors.$on-surface-variant}; + --mat-table-row-item-label-text-color: #{colors.$on-surface-variant}; + --mat-table-row-item-outline-color: #{colors.$outline-variant}; + + .table-cell-actions-container { + display: flex; + gap: 8px; } .table-cell-actions { @@ -80,10 +76,10 @@ justify-content: center; align-items: center; flex-shrink: 0; - margin: 0 2px 0 8px; + margin: 0 2px 0 12px; padding: 0; border: none; - background: $white; + background: colors.$white; cursor: pointer; &:hover { background-color: rgba(0, 0, 0, 0.04); @@ -94,7 +90,7 @@ } .filter-button.active .mat-icon { - color: mat.get-color-from-palette($color-primary, 600); + color: mat.get-theme-color($light-theme, primary, 35); } } @@ -124,11 +120,10 @@ display: flex; align-items: center; justify-content: center; - grid-row: 1/3; } .results-content-empty { - height: 100%; + @include mixins.content-empty; } .results-content-empty-message { @@ -142,11 +137,11 @@ font-weight: 400; line-height: 28px; font-size: 22px; - color: $black; + color: colors.$black; } .results-content-empty-message-main { - font-family: Roboto, sans-serif; + font-family: variables.$font-secondary; font-weight: 400; font-size: 16px; line-height: 24px; @@ -156,7 +151,7 @@ ::ng-deep .download-report-icon, .delete-report-icon { - color: $dark-grey; + color: var(--mat-table-row-item-label-text-color); } .hidden { @@ -175,6 +170,10 @@ } .download-report-zip-icon-container { - display: inline-block; - padding-top: 5px; + @include mixins.report-action; +} + +::ng-deep .mat-sort-header-container { + padding-bottom: 4px; + height: variables.$reports-table-header-size; } diff --git a/modules/ui/src/app/pages/reports/reports.component.spec.ts b/modules/ui/src/app/pages/reports/reports.component.spec.ts index 19a0a827d..840bb367b 100644 --- a/modules/ui/src/app/pages/reports/reports.component.spec.ts +++ b/modules/ui/src/app/pages/reports/reports.component.spec.ts @@ -1,4 +1,5 @@ -/** +/* +/!** * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,19 +13,23 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ -import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; + *!/ +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; -import { ReportsComponent } from './reportscomponent'; +import { ReportsComponent } from './reports.component'; import { TestRunService } from '../../services/test-run.service'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ReportsModule } from './reports.module'; import { of } from 'rxjs'; import { LiveAnnouncer } from '@angular/cdk/a11y'; import { MatDialogRef } from '@angular/material/dialog'; import { FilterDialogComponent } from './components/filter-dialog/filter-dialog.component'; import { ElementRef } from '@angular/core'; -import { FilterName } from '../../model/filters'; +import { FilterName, FilterTitle } from '../../model/filters'; import SpyObj = jasmine.SpyObj; import { MatSort } from '@angular/material/sort'; import { DATA_SOURCE_INITIAL_VALUE, ReportsStore } from './reports.store'; @@ -87,17 +92,17 @@ describe('ReportsComponent', () => { 'setFilterOpened', 'updateSort', 'getHistory', + 'getReports', ]); mockLiveAnnouncer = jasmine.createSpyObj(['announce']); TestBed.configureTestingModule({ - imports: [ReportsModule, BrowserAnimationsModule], + imports: [BrowserAnimationsModule, ReportsComponent], providers: [ { provide: TestRunService, useValue: mockService }, { provide: ReportsStore, useValue: mockReportsStore }, { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, ], - declarations: [ReportsComponent], }); TestBed.overrideProvider(ReportsStore, { useValue: mockReportsStore }); fixture = TestBed.createComponent(ReportsComponent); @@ -112,12 +117,6 @@ describe('ReportsComponent', () => { }); describe('ngOnInit', () => { - it('should set dataSource data', () => { - component.ngOnInit(); - - expect(mockReportsStore.getHistory).toHaveBeenCalled(); - }); - it('should update sort', fakeAsync(() => { const sort = new MatSort(); component.sort = sort; @@ -125,6 +124,12 @@ describe('ReportsComponent', () => { expect(mockReportsStore.updateSort).toHaveBeenCalledWith(sort); })); + + it('should get reports', fakeAsync(() => { + component.ngOnInit(); + + expect(mockReportsStore.getReports).toHaveBeenCalled(); + })); }); it('#sortData should call update sort', () => { @@ -175,13 +180,19 @@ describe('ReportsComponent', () => { } as MatDialogRef); fixture.detectChanges(); - component.openFilter(event, '', false); + component.openFilter({ + event, + filter: '', + title: '', + filterOpened: false, + }); expect(openSpy).toHaveBeenCalled(); expect(openSpy).toHaveBeenCalledWith(FilterDialogComponent, { ariaLabel: 'Filters', data: { filter: '', + title: '', trigger: new ElementRef(event.currentTarget), }, autoFocus: true, @@ -219,10 +230,30 @@ describe('ReportsComponent', () => { } as MatDialogRef); fixture.detectChanges(); - component.openFilter(event, FilterName.Started, false); - component.openFilter(event, FilterName.Results, false); - component.openFilter(event, FilterName.DeviceFirmware, false); - component.openFilter(event, FilterName.DeviceInfo, false); + component.openFilter({ + event, + filter: FilterName.Started, + title: FilterTitle.Started, + filterOpened: false, + }); + component.openFilter({ + event, + filter: FilterName.Results, + title: FilterTitle.Results, + filterOpened: false, + }); + component.openFilter({ + event, + filter: FilterName.DeviceFirmware, + title: FilterTitle.DeviceFirmware, + filterOpened: false, + }); + component.openFilter({ + event, + filter: FilterName.DeviceInfo, + title: FilterTitle.DeviceInfo, + filterOpened: false, + }); expect(mockReportsStore.setFilteredValuesResults).toHaveBeenCalledWith( mockFilterResults ); @@ -246,7 +277,7 @@ describe('ReportsComponent', () => { fixture.detectChanges(); }); - it('should focus next active element if exist', () => { + it('should focus next active element if exist', fakeAsync(() => { const row = window.document.querySelector('tbody tr') as HTMLElement; row.classList.add('report-selected'); const nextButton = window.document.querySelector( @@ -256,10 +287,12 @@ describe('ReportsComponent', () => { component.focusNextButton(); + tick(50); + expect(buttonFocusSpy).toHaveBeenCalled(); - }); + })); - it('should focus navigation button if next active element does not exist', () => { + it('should focus navigation button if next active element does not exist', fakeAsync(() => { const button = document.createElement('BUTTON'); button.classList.add('app-sidebar-button-reports'); document.querySelector('body')?.appendChild(button); @@ -267,15 +300,18 @@ describe('ReportsComponent', () => { component.focusNextButton(); + tick(50); + expect(buttonFocusSpy).toHaveBeenCalled(); - }); + })); }); it('#removeDevice should call delete report', () => { const data = HISTORY[0]; component.removeDevice(data); expect(mockReportsStore.deleteReport).toHaveBeenCalledWith({ - mac_addr: data.device.mac_addr, + mac_addr: data.mac_addr, + deviceMacAddr: data.device.mac_addr, started: data.started, }); }); @@ -321,6 +357,7 @@ describe('ReportsComponent', () => { green: false, red: true, blue: false, + cyan: false, grey: false, }); component.ngOnInit(); @@ -386,3 +423,4 @@ describe('ReportsComponent', () => { }); }); }); +*/ diff --git a/modules/ui/src/app/pages/reports/reportscomponent.ts b/modules/ui/src/app/pages/reports/reports.component.ts similarity index 64% rename from modules/ui/src/app/pages/reports/reportscomponent.ts rename to modules/ui/src/app/pages/reports/reports.component.ts index e390f0e06..b88adebf7 100644 --- a/modules/ui/src/app/pages/reports/reportscomponent.ts +++ b/modules/ui/src/app/pages/reports/reports.component.ts @@ -18,7 +18,8 @@ import { ElementRef, OnDestroy, OnInit, - ViewChild, + viewChild, + inject, } from '@angular/core'; import { LiveAnnouncer } from '@angular/cdk/a11y'; import { TestRunService } from '../../services/test-run.service'; @@ -26,45 +27,75 @@ import { StatusResultClassName, TestrunStatus, } from '../../model/testrun-status'; -import { DatePipe } from '@angular/common'; -import { MatSort, Sort } from '@angular/material/sort'; -import { Subject, takeUntil } from 'rxjs'; -import { MatRow } from '@angular/material/table'; -import { FilterDialogComponent } from './components/filter-dialog/filter-dialog.component'; +import { CommonModule, DatePipe } from '@angular/common'; +import { MatSort, MatSortModule, Sort } from '@angular/material/sort'; +import { Subject, takeUntil, timer } from 'rxjs'; +import { MatRow, MatTableModule } from '@angular/material/table'; import { MatDialog } from '@angular/material/dialog'; import { tap } from 'rxjs/internal/operators/tap'; -import { FilterName, Filters } from '../../model/filters'; +import { FilterName, FilterTitle, Filters } from '../../model/filters'; import { ReportsStore } from './reports.store'; +import { + FilterHeaderComponent, + OpenFilterEvent, +} from './components/filter-header/filter-header.component'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatIconModule } from '@angular/material/icon'; +import { FilterChipsComponent } from './components/filter-chips/filter-chips.component'; +import { DownloadReportZipComponent } from '../../components/download-report-zip/download-report-zip.component'; +import { DownloadReportPdfComponent } from '../../components/download-report-pdf/download-report-pdf.component'; +import { DeleteReportComponent } from './components/delete-report/delete-report.component'; +import { FilterDialogComponent } from './components/filter-dialog/filter-dialog.component'; +import { EmptyMessageComponent } from '../../components/empty-message/empty-message.component'; @Component({ selector: 'app-history', templateUrl: './reports.component.html', styleUrls: ['./reports.component.scss'], - providers: [ReportsStore], + imports: [ + CommonModule, + MatTableModule, + MatIconModule, + MatToolbarModule, + MatSortModule, + FilterChipsComponent, + DeleteReportComponent, + DownloadReportZipComponent, + DownloadReportPdfComponent, + FilterHeaderComponent, + EmptyMessageComponent, + ], + providers: [ReportsStore, DatePipe], }) export class ReportsComponent implements OnInit, OnDestroy { + private testRunService = inject(TestRunService); + private datePipe = inject(DatePipe); + private liveAnnouncer = inject(LiveAnnouncer); + dialog = inject(MatDialog); + private store = inject(ReportsStore); + public readonly FilterName = FilterName; + public readonly FilterTitle = FilterTitle; private destroy$: Subject = new Subject(); - @ViewChild(MatSort, { static: false }) sort!: MatSort; + sort = viewChild(MatSort); viewModel$ = this.store.viewModel$; - constructor( - private testRunService: TestRunService, - private datePipe: DatePipe, - private liveAnnouncer: LiveAnnouncer, - public dialog: MatDialog, - private store: ReportsStore - ) {} ngOnInit() { - this.store.getHistory(); - this.store.updateSort(this.sort); + this.store.getReports(); + const sort = this.sort(); + if (sort) { + this.store.updateSort(sort); + } } getFormattedDateString(date: string | null) { return date ? this.datePipe.transform(date, 'd MMM y H:mm') : ''; } sortData(sortState: Sort) { - this.store.updateSort(this.sort); + const sort = this.sort(); + if (sort) { + this.store.updateSort(sort); + } if (sortState.direction) { this.liveAnnouncer.announce(`Sorted ${sortState.direction}ending`); } else { @@ -76,22 +107,28 @@ export class ReportsComponent implements OnInit, OnDestroy { return this.testRunService.getResultClass(status); } - openFilter(event: Event, filter: string, filterOpened: boolean) { + openFilter({ event, filter, title, filterOpened }: OpenFilterEvent) { + event.preventDefault(); event.stopPropagation(); const target = new ElementRef(event.currentTarget); if (!filterOpened) { - this.openFilterDialog(target, filter); + this.openFilterDialog(target, filter, title); } } - openFilterDialog(target: ElementRef, filter: string) { + openFilterDialog( + target: ElementRef, + filter: string, + title: string + ) { this.store.setFilterOpened(true); this.store.setActiveFiler(filter); const dialogRef = this.dialog.open(FilterDialogComponent, { ariaLabel: 'Filters', data: { filter, + title, trigger: target, }, autoFocus: true, @@ -152,19 +189,24 @@ export class ReportsComponent implements OnInit, OnDestroy { '.report-selected + tr a' ) as HTMLButtonElement; if (next) { - next.focus(); + timer(50).subscribe(() => { + next.focus(); + }); } else { // If next interactive element doest not exist, add menu reports button should be focused const menuButton = window.document.querySelector( '.app-sidebar-button-reports' ) as HTMLButtonElement; - menuButton?.focus(); + timer(50).subscribe(() => { + menuButton?.focus(); + }); } } removeDevice(data: TestrunStatus) { this.store.deleteReport({ mac_addr: data.mac_addr, + deviceMacAddr: data.device.mac_addr, started: data.started, }); this.focusNextButton(); diff --git a/modules/ui/src/app/pages/reports/reports.module.ts b/modules/ui/src/app/pages/reports/reports.module.ts deleted file mode 100644 index f218780c5..000000000 --- a/modules/ui/src/app/pages/reports/reports.module.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { NgModule } from '@angular/core'; -import { CommonModule, DatePipe } from '@angular/common'; -import { ReportsComponent } from './reportscomponent'; -import { ReportsRoutingModule } from './reports-routing.module'; -import { MatTableModule } from '@angular/material/table'; -import { MatIconModule } from '@angular/material/icon'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { DownloadReportComponent } from '../../components/download-report/download-report.component'; -import { MatSortModule } from '@angular/material/sort'; -import { FilterDialogComponent } from './components/filter-dialog/filter-dialog.component'; -import { FilterChipsComponent } from './components/filter-chips/filter-chips.component'; -import { DeleteReportComponent } from './components/delete-report/delete-report.component'; -import { DownloadReportZipComponent } from '../../components/download-report-zip/download-report-zip.component'; -import { DownloadReportPdfComponent } from '../../components/download-report-pdf/download-report-pdf.component'; - -@NgModule({ - declarations: [ReportsComponent], - imports: [ - CommonModule, - ReportsRoutingModule, - MatTableModule, - MatIconModule, - MatToolbarModule, - MatSortModule, - DownloadReportComponent, - FilterDialogComponent, - FilterChipsComponent, - DeleteReportComponent, - DownloadReportZipComponent, - DownloadReportPdfComponent, - ], - providers: [DatePipe], -}) -export class ReportsModule {} diff --git a/modules/ui/src/app/pages/reports/reports.store.spec.ts b/modules/ui/src/app/pages/reports/reports.store.spec.ts index e78b40036..e6b7ec2f3 100644 --- a/modules/ui/src/app/pages/reports/reports.store.spec.ts +++ b/modules/ui/src/app/pages/reports/reports.store.spec.ts @@ -18,9 +18,8 @@ import { TestRunService } from '../../services/test-run.service'; import SpyObj = jasmine.SpyObj; import { TestBed } from '@angular/core/testing'; import { skip, take } from 'rxjs'; -import { throwError } from 'rxjs/internal/observable/throwError'; import { of } from 'rxjs/internal/observable/of'; -import { DATA_SOURCE_INITIAL_VALUE, ReportsStore } from './reports.store'; +import { ReportsStore } from './reports.store'; import { EMPTY_FILTERS, FILTERS, @@ -29,31 +28,39 @@ import { HISTORY_AFTER_REMOVE, } from '../../mocks/reports.mock'; import { DatePipe } from '@angular/common'; -import { HttpErrorResponse } from '@angular/common/http'; import { MatRow } from '@angular/material/table'; import { MatSort } from '@angular/material/sort'; -import { provideMockStore } from '@ngrx/store/testing'; -import { selectRiskProfiles } from '../../store/selectors'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { selectReports, selectRiskProfiles } from '../../store/selectors'; +import { AppState } from '../../store/state'; +import { setReports } from '../../store/actions'; describe('ReportsStore', () => { let reportsStore: ReportsStore; let mockService: SpyObj; + let store: MockStore; beforeEach(() => { - mockService = jasmine.createSpyObj(['getHistory', 'deleteReport']); + mockService = jasmine.createSpyObj(['deleteReport']); TestBed.configureTestingModule({ providers: [ ReportsStore, { provide: TestRunService, useValue: mockService }, provideMockStore({ - selectors: [{ selector: selectRiskProfiles, value: [] }], + selectors: [ + { selector: selectRiskProfiles, value: [] }, + { selector: selectReports, value: [] }, + ], }), DatePipe, ], }); reportsStore = TestBed.inject(ReportsStore); + store = TestBed.inject(MockStore); + + spyOn(store, 'dispatch').and.callFake(() => {}); }); it('should be created', () => { @@ -110,7 +117,7 @@ describe('ReportsStore', () => { }); it('should update dataSource', (done: DoneFn) => { - reportsStore.viewModel$.pipe(skip(2), take(1)).subscribe(store => { + reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { expect(store.dataSource.data).toEqual(FORMATTED_HISTORY); expect(store.dataLoaded).toEqual(true); done(); @@ -129,11 +136,12 @@ describe('ReportsStore', () => { 'duration', 'deviceInfo', 'deviceFirmware', + 'program', 'status', 'report', ], chips: ['chips'], - dataSource: DATA_SOURCE_INITIAL_VALUE, + dataSource: store.dataSource, filterOpened: false, activeFilter: '', filteredValues: { @@ -142,7 +150,7 @@ describe('ReportsStore', () => { results: [], dateRange: '', }, - dataLoaded: false, + dataLoaded: true, selectedRow: null, isFiltersEmpty: true, profiles: [], @@ -154,56 +162,59 @@ describe('ReportsStore', () => { describe('effects', () => { describe('getHistory', () => { - describe('should update store', () => { - it('with empty value if error happens', done => { - mockService.getHistory.and.returnValue( - throwError( - new HttpErrorResponse({ error: { error: 'error' }, status: 500 }) - ) - ); - - reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.dataSource.data).toEqual([]); - done(); - }); + it('should update store', done => { + store.overrideSelector(selectReports, [...HISTORY]); + store.refreshState(); - reportsStore.getHistory(); + reportsStore.viewModel$.pipe(take(1)).subscribe(store => { + expect(store.dataSource.data).toEqual(FORMATTED_HISTORY); + done(); }); - it('with value if not null', done => { - mockService.getHistory.and.returnValue(of([...HISTORY])); - - reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.dataSource.data).toEqual(FORMATTED_HISTORY); - done(); - }); - - reportsStore.getHistory(); - }); + reportsStore.getHistory(); }); }); describe('deleteReport', () => { it('should update store', done => { mockService.deleteReport.and.returnValue(of(true)); - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); + store.refreshState(); - reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.dataSource.data).toEqual(HISTORY_AFTER_REMOVE); - done(); + reportsStore.deleteReport({ + mac_addr: '01:02:03:04:05:07', + deviceMacAddr: '01:02:03:04:05:07', + started: '2023-07-23T10:11:00.123Z', }); + expect(store.dispatch).toHaveBeenCalledWith( + setReports({ reports: HISTORY_AFTER_REMOVE }) + ); + done(); + }); + + it('should update store after remove with null mac_addr', done => { + mockService.deleteReport.and.returnValue(of(true)); + store.overrideSelector(selectReports, [...HISTORY_AFTER_REMOVE]); + store.refreshState(); + reportsStore.deleteReport({ - mac_addr: '00:1e:42:35:73:c4', - started: '2023-06-22T10:11:00.123Z', + mac_addr: null, + deviceMacAddr: '01:02:03:04:05:08', + started: '2023-06-23T10:11:00.123Z', }); + + expect(store.dispatch).toHaveBeenCalledWith( + setReports({ reports: [HISTORY_AFTER_REMOVE[0]] }) + ); + done(); }); }); describe('updateSort', () => { it('should update store', done => { const sort = new MatSort(); - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.updateSort(sort); @@ -217,7 +228,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesResults', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ results: ['test2'] } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesResults(['test2']); @@ -235,7 +246,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesDeviceInfo', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ deviceInfo: 'test2' } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesDeviceInfo('test2'); @@ -253,7 +264,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesDeviceFirmware', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ deviceFirmware: 'test2' } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesDeviceFirmware('test2'); @@ -271,7 +282,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesDateRange', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ dateRange: 'test2' } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesDateRange('test2'); @@ -289,7 +300,7 @@ describe('ReportsStore', () => { describe('setFilteredValues', () => { it('should update store', done => { const updatedFilters = { ...EMPTY_FILTERS }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValues(updatedFilters); diff --git a/modules/ui/src/app/pages/reports/reports.store.ts b/modules/ui/src/app/pages/reports/reports.store.ts index ab93a1085..ded8bf4f7 100644 --- a/modules/ui/src/app/pages/reports/reports.store.ts +++ b/modules/ui/src/app/pages/reports/reports.store.ts @@ -1,16 +1,18 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { ComponentStore } from '@ngrx/component-store'; import { MatRow, MatTableDataSource } from '@angular/material/table'; import { HistoryTestrun, TestrunStatus } from '../../model/testrun-status'; import { DateRange, Filters } from '../../model/filters'; import { TestRunService } from '../../services/test-run.service'; -import { catchError, EMPTY, exhaustMap } from 'rxjs'; +import { exhaustMap } from 'rxjs'; import { tap, withLatestFrom } from 'rxjs/operators'; import { DatePipe } from '@angular/common'; import { MatSort } from '@angular/material/sort'; -import { selectRiskProfiles } from '../../store/selectors'; +import { selectReports, selectRiskProfiles } from '../../store/selectors'; import { Store } from '@ngrx/store'; import { AppState } from '../../store/state'; +import { fetchReports, setReports } from '../../store/actions'; +import { TestingType } from '../../model/device'; export interface ReportsComponentState { displayedColumns: string[]; @@ -28,9 +30,12 @@ export interface ReportsComponentState { export const DATA_SOURCE_INITIAL_VALUE = new MatTableDataSource( [] ); - @Injectable() export class ReportsStore extends ComponentStore { + private store = inject>(Store); + private testRunService = inject(TestRunService); + private datePipe = inject(DatePipe); + private displayedColumns$ = this.select(state => state.displayedColumns); private chips$ = this.select(state => state.chips); private dataSource$ = this.select(state => state.dataSource); @@ -40,7 +45,7 @@ export class ReportsStore extends ComponentStore { private dataLoaded$ = this.select(state => state.dataLoaded); private selectedRow$ = this.select(state => state.selectedRow); private isFiltersEmpty$ = this.select(state => state.isFiltersEmpty); - private history$ = this.select(state => state.history); + private history$ = this.store.select(selectReports); private profiles$ = this.store.select(selectRiskProfiles); viewModel$ = this.select({ displayedColumns: this.displayedColumns$, @@ -105,13 +110,6 @@ export class ReportsStore extends ComponentStore { }; }); - setHistory = this.updater((state, history: TestrunStatus[]) => { - return { - ...state, - history, - }; - }); - updateFilteredValues = this.updater((state, filteredValues: Filters) => { return { ...state, @@ -119,42 +117,23 @@ export class ReportsStore extends ComponentStore { }; }); - getHistory = this.effect(trigger$ => { - return trigger$.pipe( - exhaustMap(() => { - return this.testRunService.getHistory().pipe( - withLatestFrom(this.filteredValues$), - tap(([reports, filteredValues]) => { - if (reports) { - this.setDataSource(reports); - this.setFilteredValues(filteredValues); - this.setHistory(reports); - } - }), - catchError(() => { - this.setDataSource([]); - this.setHistory([]); - return EMPTY; - }) - ); - }) - ); - }); - deleteReport = this.effect<{ - mac_addr: string; + mac_addr: string | null; + deviceMacAddr: string; started: string | null; }>(trigger$ => { return trigger$.pipe( - exhaustMap(({ mac_addr, started }) => { - return this.testRunService.deleteReport(mac_addr, started || '').pipe( - withLatestFrom(this.history$), - tap(([remove, current]) => { - if (remove) { - this.removeReport(mac_addr, started, current); - } - }) - ); + exhaustMap(({ mac_addr, deviceMacAddr, started }) => { + return this.testRunService + .deleteReport(mac_addr || deviceMacAddr, started || '') + .pipe( + withLatestFrom(this.history$), + tap(([remove, current]) => { + if (remove) { + this.removeReport(mac_addr, deviceMacAddr, started, current); + } + }) + ); }) ); }); @@ -225,19 +204,40 @@ export class ReportsStore extends ComponentStore { ); }); + getHistory = this.effect(() => { + return this.history$.pipe( + withLatestFrom(this.filteredValues$), + tap(([reports, filteredValues]) => { + this.setDataSource([...reports]); + this?.setFilteredValues(filteredValues); + }) + ); + }); + + getReports = this.effect(trigger$ => { + return trigger$.pipe( + tap(() => { + this.store.dispatch(fetchReports()); + }) + ); + }); + private removeReport( - mac_addr: string, + mac_addr: string | null, + deviceMacAddr: string, started: string | null, current: TestrunStatus[] ) { - const history = current; + const history = [...current]; const idx = history.findIndex( - report => report.mac_addr === mac_addr && report.started === started + report => + report.mac_addr === mac_addr && + report.device.mac_addr === deviceMacAddr && + report.started === started ); if (typeof idx === 'number') { history.splice(idx, 1); - this.setHistory(history); - this.setDataSource(history); + this.store.dispatch(setReports({ reports: history })); } } @@ -257,11 +257,24 @@ export class ReportsStore extends ComponentStore { ...item, deviceFirmware: item.device.firmware, deviceInfo: item.device.manufacturer + ' ' + item.device.model, + testResult: this.getTestResult(item), duration: this.getDuration(item.started, item.finished), + program: item.device.test_pack ?? '', }; }); } + private getTestResult(item: TestrunStatus): string { + let result = ''; + if (item.device.test_pack === TestingType.Qualification) { + result = item.result ? item.result : item.status; + } + if (item.device.test_pack === TestingType.Pilot) { + result = item.status; + } + return result; + } + private getDuration(started: string | null, finished: string | null): string { if (!started || !finished) { return ''; @@ -294,7 +307,7 @@ export class ReportsStore extends ComponentStore { const isIncludeStatus = searchString.results?.length === 0 || - searchString.results?.includes(data.status); + searchString.results?.includes(data.testResult); const isIncludeStartedDate = this.filterStartedDateRange( data.started, searchString @@ -361,17 +374,14 @@ export class ReportsStore extends ComponentStore { return value.length === 0; }); } - constructor( - private store: Store, - private testRunService: TestRunService, - private datePipe: DatePipe - ) { + constructor() { super({ displayedColumns: [ 'started', 'duration', 'deviceInfo', 'deviceFirmware', + 'program', 'status', 'report', ], diff --git a/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.html b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.html new file mode 100644 index 000000000..341041168 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.html @@ -0,0 +1,45 @@ + +Risk assessment completed +

+ The risk profile has been saved as "{{ data.profile.name }}" and can now be + attached to test reports. +

+

+ The preliminary risk estimation based on your answers is + + {{ data.profile.risk }} risk + + + +
{{ getRiskExplanation(data.profile.risk) }} The full report can be found + in the zip file. Please share with the lab to validate this profile and + determine next steps. +

+ + + diff --git a/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.scss b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.scss new file mode 100644 index 000000000..4842b04e0 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.scss @@ -0,0 +1,70 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use 'colors'; +@use 'variables'; +@use 'mixins'; + +::ng-deep :root { + --mat-dialog-container-max-width: 560px; +} + +:host { + @include mixins.dialog; + padding: 24px 0 16px 0; + gap: 16px; + > * { + padding: 0 24px; + } +} + +.simple-dialog-title { + font-family: variables.$font-primary; + font-size: 24px; + line-height: 32px; + text-align: center; + color: colors.$on-surface; +} + +.simple-dialog-content { + font-family: variables.$font-text; + font-size: 14px; + line-height: 20px; + letter-spacing: 0; + color: colors.$on-surface-variant; + margin: 0; +} + +.simple-dialog-actions { + padding: 0; + min-height: 30px; +} + +.simple-dialog-content-risk { + font-weight: bold; + display: inline-flex; + align-items: center; +} + +.profile-item-risk { + display: inline-flex; + align-items: center; + height: 20px; + margin-left: 2px; + font-family: variables.$font-secondary; + font-size: 12px; + font-weight: 400; + letter-spacing: 0.3px; +} diff --git a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.spec.ts similarity index 50% rename from modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.spec.ts rename to modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.spec.ts index d7cd5abb6..b3a40c1bc 100644 --- a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.spec.ts @@ -15,30 +15,24 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ShutdownAppModalComponent } from './shutdown-app-modal.component'; -import { - MAT_DIALOG_DATA, - MatDialogModule, - MatDialogRef, -} from '@angular/material/dialog'; -import { MatButtonModule } from '@angular/material/button'; +import { SuccessDialogComponent } from './success-dialog.component'; +import { TestRunService } from '../../../../services/test-run.service'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { of } from 'rxjs'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { PROFILE_MOCK } from '../../../../mocks/profile.mock'; +import { ProfileRisk } from '../../../../model/profile'; -describe('ShutdownAppModalComponent', () => { - let component: ShutdownAppModalComponent; - let fixture: ComponentFixture; +describe('SuccessDialogComponent', () => { + let component: SuccessDialogComponent; + let fixture: ComponentFixture; + const testRunServiceMock = jasmine.createSpyObj(['getRiskClass']); let compiled: HTMLElement; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - ShutdownAppModalComponent, - MatDialogModule, - MatButtonModule, - BrowserAnimationsModule, - ], + imports: [SuccessDialogComponent], providers: [ + { provide: TestRunService, useValue: testRunServiceMock }, { provide: MatDialogRef, useValue: { @@ -49,12 +43,10 @@ describe('ShutdownAppModalComponent', () => { { provide: MAT_DIALOG_DATA, useValue: {} }, ], }).compileComponents(); - - fixture = TestBed.createComponent(ShutdownAppModalComponent); + fixture = TestBed.createComponent(SuccessDialogComponent); component = fixture.componentInstance; component.data = { - title: 'title', - content: 'content', + profile: PROFILE_MOCK, }; compiled = fixture.nativeElement as HTMLElement; fixture.detectChanges(); @@ -64,37 +56,23 @@ describe('ShutdownAppModalComponent', () => { expect(component).toBeTruthy(); }); - it('should has title and content', () => { - const title = compiled.querySelector('.modal-title') as HTMLElement; - const content = compiled.querySelector('.modal-content') as HTMLElement; - - expect(title.innerHTML.trim()).toEqual('title'); - expect(content.innerHTML.trim()).toEqual('content'); - }); - - it('should close dialog on click "cancel" button', () => { - const closeSpy = spyOn(component.dialogRef, 'close'); - const closeButton = compiled.querySelector( - '.cancel-button' - ) as HTMLButtonElement; - - closeButton.click(); - - expect(closeSpy).toHaveBeenCalledWith(); - - closeSpy.calls.reset(); - }); - - it('should close dialog with true on click "confirm" button', () => { + it('should close dialog on "cancel" click', () => { const closeSpy = spyOn(component.dialogRef, 'close'); const confirmButton = compiled.querySelector( '.confirm-button' ) as HTMLButtonElement; - confirmButton.click(); + confirmButton?.click(); - expect(closeSpy).toHaveBeenCalledWith(true); + expect(closeSpy).toHaveBeenCalled(); closeSpy.calls.reset(); }); + + it('should return proper text for risk', () => { + expect(component.getRiskExplanation(ProfileRisk.LIMITED)).toEqual(''); + expect(component.getRiskExplanation(ProfileRisk.HIGH)).toEqual( + 'An additional assessment may be required.' + ); + }); }); diff --git a/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.ts b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.ts new file mode 100644 index 000000000..d777dc5c2 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.ts @@ -0,0 +1,69 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, inject } from '@angular/core'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; +import { + Profile, + ProfileRisk, + RiskResultClassName, +} from '../../../../model/profile'; +import { TestRunService } from '../../../../services/test-run.service'; +import { CommonModule } from '@angular/common'; + +interface DialogData { + profile: Profile; +} + +@Component({ + selector: 'app-success-dialog', + templateUrl: './success-dialog.component.html', + styleUrls: ['./success-dialog.component.scss'], + + imports: [MatDialogModule, MatButtonModule, CommonModule], +}) +export class SuccessDialogComponent extends EscapableDialogComponent { + private readonly testRunService = inject(TestRunService); + override dialogRef: MatDialogRef; + data = inject(MAT_DIALOG_DATA); + + constructor() { + const dialogRef = + inject>(MatDialogRef); + + super(); + this.dialogRef = dialogRef; + } + + confirm() { + this.dialogRef.close(); + } + + public getRiskClass(riskResult: string): RiskResultClassName { + return this.testRunService.getRiskClass(riskResult); + } + + getRiskExplanation(risk: string | undefined) { + return risk === ProfileRisk.HIGH + ? 'An additional assessment may be required.' + : ''; + } +} diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html index e5d0ae0cb..a95664895 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html @@ -15,13 +15,18 @@ -->
-

Profile name *

+ - Specify risk assessment profile name - + class="profile-form-field"> + Required for saving a profile The Profile name is required - This Profile name is already used for another Risk Assessment - profile + This Profile name is already used for another profile - - - + +
+
+ + +
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ description }} - - Please, check. “ and \ are not allowed. - - - The field is required - - - The field must be a maximum of - {{ getControl(formControlName).getError('maxlength').requiredLength }} - characters. - - - - - - - - {{ description }} - - Please, check. “ and \ are not allowed. - - - The field is required - - - The field must be a maximum of - {{ getControl(formControlName).getError('maxlength').requiredLength }} - characters. - - - - - - - - {{ description }} - - The field is required - - - Please, check the email address. Valid e-mail can contain only latin - letters, numbers, @ and . (dot). - - - The field must be a maximum of - {{ getControl(formControlName).getError('maxlength').requiredLength }} - characters. - - - - - -
-

- - {{ option }} - -

- {{ - description - }} -
-
- - - - - - {{ option }} - - - {{ - description - }} - - The field is required - - - diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss index d9e90a2c4..2de1ed1ae 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss @@ -14,70 +14,60 @@ * limitations under the License. */ @use '@angular/material' as mat; -@import 'src/theming/colors'; -@import 'src/theming/variables'; +@use 'colors'; +@use 'variables'; +@use 'mixins'; + +:host { + height: 100%; + display: flex; + flex-direction: column; +} .profile-form { + overflow-y: scroll; + + .name-field-label { + margin: 0; + } .field-container { display: flex; flex-direction: column; align-items: flex-start; - padding: 8px 16px 8px 24px; - } - .field-label { - margin: 0; - color: $grey-800; - font-size: 18px; - line-height: 24px; - padding-top: 24px; - padding-bottom: 16px; - &:first-child { - padding-top: 0; - } - &:has(+ .field-select-multiple.ng-invalid.ng-dirty) { - color: mat.get-color-from-palette($color-warn, 700); - } + padding: 16px 32px 11px 32px; } - mat-form-field { + + .profile-form-field { width: 100%; } - .field-hint { - font-family: $font-secondary; - font-size: 12px; - font-weight: 400; - line-height: 16px; - text-align: left; - padding-top: 8px; - } } -.profile-form-field { - width: 100%; +.profile-form-field ::ng-deep .mat-mdc-form-field-textarea-control { + display: inherit; } .form-actions { - display: flex; - gap: 16px; - padding: 8px 24px 24px 24px; -} + @include mixins.form-actions; -.save-draft-button:not(.mat-mdc-button-disabled) { - color: $primary; -} + div { + display: flex; + gap: 12px; + } -.field-select-multiple { - .field-select-checkbox { - &:has(::ng-deep .mat-mdc-checkbox-checked) { - background: mat.get-color-from-palette($color-primary, 50); - } - ::ng-deep .mdc-checkbox__ripple { - display: none; - } - &:first-of-type { - margin-top: 0; - } - &:last-of-type { - margin-bottom: 8px; - } + .save-draft-button:not(.mat-mdc-button-disabled), + .copy-button:not(.mat-mdc-button-disabled), + .discard-button:not(.mat-mdc-button-disabled) { + @include mixins.secondary-button; } + + .delete-button:not(.mat-mdc-button-disabled) { + @include mixins.delete-red-button; + } +} + +.save-profile-button:not(.mat-mdc-button-disabled), +.save-draft-button:not(.mat-mdc-button-disabled), +.discard-button:not(.mat-mdc-button-disabled) { + cursor: pointer; + pointer-events: auto; } diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts index 7344dd92c..8691cbcf9 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts @@ -13,29 +13,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; import { ProfileFormComponent } from './profile-form.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { + COPY_PROFILE_MOCK, + DRAFT_COPY_PROFILE_MOCK, NEW_PROFILE_MOCK, NEW_PROFILE_MOCK_DRAFT, + OUTDATED_DRAFT_PROFILE_MOCK, PROFILE_FORM, PROFILE_MOCK, PROFILE_MOCK_2, PROFILE_MOCK_3, RENAME_PROFILE_MOCK, } from '../../../mocks/profile.mock'; -import { FormControlType, ProfileStatus } from '../../../model/profile'; +import { ProfileStatus } from '../../../model/profile'; +import { RiskAssessmentStore } from '../risk-assessment.store'; +import { TestRunService } from '../../../services/test-run.service'; +import { provideMockStore } from '@ngrx/store/testing'; +import { of } from 'rxjs'; +import { MatDialogRef } from '@angular/material/dialog'; +import { SimpleDialogComponent } from '../../../components/simple-dialog/simple-dialog.component'; describe('ProfileFormComponent', () => { let component: ProfileFormComponent; let fixture: ComponentFixture; let compiled: HTMLElement; + const testrunServiceMock: jasmine.SpyObj = + jasmine.createSpyObj('testrunServiceMock', [ + 'fetchQuestionnaireFormat', + 'saveDevice', + ]); beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ProfileFormComponent, BrowserAnimationsModule], + providers: [ + RiskAssessmentStore, + { provide: TestRunService, useValue: testrunServiceMock }, + provideMockStore({}), + ], }).compileComponents(); fixture = TestBed.createComponent(ProfileFormComponent); @@ -87,6 +106,9 @@ describe('ProfileFormComponent', () => { [ 'very long value very long value very long value very long value very long value very long value very long value', 'as&@3$', + 'test/', + 'test[', + ':test', ].forEach(value => { const name: HTMLInputElement = compiled.querySelector( '.form-name' @@ -132,7 +154,6 @@ describe('ProfileFormComponent', () => { name.dispatchEvent(new Event('input')); component.nameControl.markAsTouched(); - fixture.detectChanges(); fixture.detectChanges(); const nameError = compiled.querySelector('mat-error')?.innerHTML; @@ -140,180 +161,11 @@ describe('ProfileFormComponent', () => { expect(error).toBeTruthy(); expect(nameError).toContain( - 'This Profile name is already used for another Risk Assessment profile' + 'This Profile name is already used for another profile' ); }); }); - PROFILE_FORM.forEach((item, index) => { - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - - it(`should have form field with specific type"`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - - if (item.type === FormControlType.SELECT) { - const select = fields[uiIndex].querySelector('mat-select'); - expect(select).toBeTruthy(); - } else if (item.type === FormControlType.SELECT_MULTIPLE) { - const select = fields[uiIndex].querySelector('mat-checkbox'); - expect(select).toBeTruthy(); - } else if (item.type === FormControlType.TEXTAREA) { - const input = fields[uiIndex]?.querySelector('textarea'); - expect(input).toBeTruthy(); - } else { - const input = fields[uiIndex]?.querySelector('input'); - expect(input).toBeTruthy(); - } - }); - - it('should have label', () => { - const labels = compiled.querySelectorAll('.field-label'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - - const label = item?.validation?.required - ? item.question + ' *' - : item.question; - expect(labels[uiIndex].textContent?.trim()).toEqual(label); - }); - - it('should have hint', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const hint = fields[uiIndex].querySelector('mat-hint'); - - if (item.description) { - expect(hint?.textContent?.trim()).toEqual(item.description); - } else { - expect(hint).toBeNull(); - } - }); - - if (item.type === FormControlType.SELECT) { - describe('select', () => { - it(`should have default value if provided`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const select = fields[uiIndex].querySelector('mat-select'); - expect(select?.textContent?.trim()).toEqual(item.default || ''); - }); - - it('should have "required" error when field is not filled', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - - component.getControl(index).setValue(''); - component.getControl(index).markAsTouched(); - - fixture.detectChanges(); - - const error = fields[uiIndex].querySelector('mat-error')?.innerHTML; - - expect(error).toContain('The field is required'); - }); - }); - } - - if (item.type === FormControlType.SELECT_MULTIPLE) { - describe('select multiple', () => { - it(`should mark form group as dirty while tab navigation`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const checkbox = fields[uiIndex].querySelector( - '.field-select-checkbox:last-of-type mat-checkbox' - ); - checkbox?.dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab' }) - ); - fixture.detectChanges(); - - expect(component.getControl(index).dirty).toBeTrue(); - }); - }); - } - - if ( - item.type === FormControlType.TEXT || - item.type === FormControlType.TEXTAREA || - item.type === FormControlType.EMAIL_MULTIPLE - ) { - describe('text or text-long or email-multiple', () => { - if (item.validation?.required) { - it('should have "required" error when field is not filled', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const input = fields[uiIndex].querySelector( - '.mat-mdc-input-element' - ) as HTMLInputElement; - ['', ' '].forEach(value => { - input.value = value; - input.dispatchEvent(new Event('input')); - component.getControl(index).markAsTouched(); - fixture.detectChanges(); - const errors = fields[uiIndex].querySelectorAll('mat-error'); - let hasError = false; - errors.forEach(error => { - if (error.textContent === 'The field is required') { - hasError = true; - } - }); - - expect(hasError).toBeTrue(); - }); - }); - } - - it('should have "invalid_format" error when field does not satisfy validation rules', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const input: HTMLInputElement = fields[uiIndex].querySelector( - '.mat-mdc-input-element' - ) as HTMLInputElement; - input.value = 'as\\\\\\\\\\""""""""'; - input.dispatchEvent(new Event('input')); - component.getControl(index).markAsTouched(); - fixture.detectChanges(); - const result = - item.type === FormControlType.EMAIL_MULTIPLE - ? 'Please, check the email address. Valid e-mail can contain only latin letters, numbers, @ and . (dot).' - : 'Please, check. “ and \\ are not allowed.'; - const errors = fields[uiIndex].querySelectorAll('mat-error'); - let hasError = false; - errors.forEach(error => { - if (error.textContent === result) { - hasError = true; - } - }); - - expect(hasError).toBeTrue(); - }); - - if (item.validation?.max) { - it('should have "maxlength" error when field is exceeding max length', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const input: HTMLInputElement = fields[uiIndex].querySelector( - '.mat-mdc-input-element' - ) as HTMLInputElement; - input.value = - 'very long value very long value very long value very long value very long value very long value very long value very long value very long value very long value'; - input.dispatchEvent(new Event('input')); - component.getControl(index).markAsTouched(); - fixture.detectChanges(); - - const errors = fields[uiIndex].querySelectorAll('mat-error'); - let hasError = false; - errors.forEach(error => { - if ( - error.textContent === - `The field must be a maximum of ${item.validation?.max} characters.` - ) { - hasError = true; - } - }); - expect(hasError).toBeTrue(); - }); - } - }); - } - }); - describe('Draft button', () => { it('should be disabled when profile name is empty', () => { component.nameControl.setValue(''); @@ -388,9 +240,52 @@ describe('ProfileFormComponent', () => { }); }); }); + + describe('Discard button', () => { + beforeEach(() => { + fillForm(component); + fixture.detectChanges(); + }); + + it('should be enabled when form is filled', () => { + const discardButton = compiled.querySelector( + '.discard-button' + ) as HTMLButtonElement; + + expect(discardButton.disabled).toBeFalse(); + }); + + it('should emit discard', () => { + const emitSpy = spyOn(component.discard, 'emit'); + const discardButton = compiled.querySelector( + '.discard-button' + ) as HTMLButtonElement; + discardButton.click(); + + expect(emitSpy).toHaveBeenCalled(); + }); + }); }); describe('Class tests', () => { + describe('with outdated draft profile', () => { + beforeEach(() => { + component.selectedProfile = OUTDATED_DRAFT_PROFILE_MOCK; + fixture.detectChanges(); + }); + + it('should have an error when uses the name of copy profile', () => { + expect(component.profileForm.value).toEqual({ + 0: '', + 1: 'IoT Sensor', + 2: '', + 3: { 0: false, 1: false, 2: false }, + 4: '', + name: 'Outdated profile', + }); + }); + }); + describe('with profile', () => { beforeEach(() => { component.selectedProfile = PROFILE_MOCK; @@ -432,6 +327,16 @@ describe('ProfileFormComponent', () => { component.nameControl.hasError('has_same_profile_name') ).toBeTrue(); }); + + it('should have an error when uses the name of copy profile', fakeAsync(() => { + component.selectedProfile = DRAFT_COPY_PROFILE_MOCK; + component.profiles = [PROFILE_MOCK, PROFILE_MOCK_2, COPY_PROFILE_MOCK]; + fixture.detectChanges(); + + expect( + component.nameControl.hasError('has_same_profile_name') + ).toBeTrue(); + })); }); describe('with no profile', () => { @@ -448,6 +353,31 @@ describe('ProfileFormComponent', () => { expect(emitSpy).toHaveBeenCalledWith(NEW_PROFILE_MOCK); }); }); + + describe('openCloseDialog', () => { + it('should open discard modal', fakeAsync(() => { + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of(true), + } as MatDialogRef); + + component.openCloseDialog(); + + expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, { + ariaLabel: 'Discard the Risk Assessment changes', + data: { + title: 'Discard changes?', + content: `You have unsaved changes that would be permanently lost.`, + confirmName: 'Discard', + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: ['simple-dialog', 'discard-dialog'], + }); + + openSpy.calls.reset(); + })); + }); }); function fillForm(component: ProfileFormComponent) { diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts index a15867ae7..29b5eb2f0 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts @@ -16,6 +16,7 @@ import { CdkTextareaAutosize, TextFieldModule } from '@angular/cdk/text-field'; import { afterNextRender, + AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, @@ -24,8 +25,7 @@ import { Input, OnInit, Output, - QueryList, - ViewChildren, + viewChildren, } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatError, MatFormFieldModule } from '@angular/material/form-field'; @@ -39,23 +39,26 @@ import { FormGroup, ReactiveFormsModule, ValidatorFn, - Validators, } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; -import { DeviceValidators } from '../../devices/components/device-form/device.validators'; import { - FormControlType, Profile, ProfileFormat, ProfileStatus, Question, - Validation, } from '../../../model/profile'; +import { FormControlType } from '../../../model/question'; import { ProfileValidators } from './profile.validators'; +import { DynamicFormComponent } from '../../../components/dynamic-form/dynamic-form.component'; +import { Observable } from 'rxjs/internal/Observable'; +import { of } from 'rxjs/internal/observable/of'; +import { map } from 'rxjs/internal/operators/map'; +import { SimpleDialogComponent } from '../../../components/simple-dialog/simple-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { RiskAssessmentStore } from '../risk-assessment.store'; @Component({ selector: 'app-profile-form', - standalone: true, imports: [ MatButtonModule, CommonModule, @@ -66,27 +69,32 @@ import { ProfileValidators } from './profile.validators'; MatSelectModule, MatCheckboxModule, TextFieldModule, + DynamicFormComponent, ], templateUrl: './profile-form.component.html', styleUrl: './profile-form.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ProfileFormComponent implements OnInit { +export class ProfileFormComponent implements OnInit, AfterViewInit { + private profileValidators = inject(ProfileValidators); + private fb = inject(FormBuilder); + private store = inject(RiskAssessmentStore); private profile: Profile | null = null; private profileList!: Profile[]; private injector = inject(Injector); private nameValidator!: ValidatorFn; - public readonly FormControlType = FormControlType; + private changeProfile = true; public readonly ProfileStatus = ProfileStatus; profileForm: FormGroup = this.fb.group({}); - @ViewChildren(CdkTextareaAutosize) - autosize!: QueryList; + dialog = inject(MatDialog); + readonly autosize = viewChildren(CdkTextareaAutosize); @Input() profileFormat!: ProfileFormat[]; + @Input() isCopyProfile!: boolean; @Input() set profiles(profiles: Profile[]) { this.profileList = profiles; - if (this.nameControl) { - this.updateNameValidator(); + if (this.nameControl && this.profile) { + this.updateNameValidator(this.profile); } } get profiles() { @@ -94,31 +102,155 @@ export class ProfileFormComponent implements OnInit { } @Input() set selectedProfile(profile: Profile | null) { - this.profile = profile; - if (profile && this.nameControl) { - this.updateNameValidator(); - this.fillProfileForm(this.profileFormat, profile); + if (this.isCopyProfile && this.profile) { + this.deleteCopy.emit(this.profile); + } + if (this.changeProfile || this.profileHasNoChanges()) { + this.changeProfile = false; + this.profile = profile; + if (profile && this.nameControl) { + this.updateNameValidator(profile); + this.fillProfileForm(this.profileFormat, profile); + } else { + this.profileForm.reset(); + } + } else if (this.profile != profile) { + // prevent select profile before user confirmation + this.store.updateSelectedProfile(this.profile); + this.openCloseDialogToChangeProfile(profile); } } + get selectedProfile() { return this.profile; } @Output() saveProfile = new EventEmitter(); - constructor( - private deviceValidators: DeviceValidators, - private profileValidators: ProfileValidators, - private fb: FormBuilder - ) {} + @Output() deleteCopy = new EventEmitter(); + @Output() discard = new EventEmitter(); ngOnInit() { - this.profileForm = this.createProfileForm(this.profileFormat); + this.profileForm = this.createProfileForm(); + } + + ngAfterViewInit(): void { if (this.selectedProfile) { - this.fillProfileForm(this.profileFormat, this.selectedProfile); + this.fillProfileForm(this.profileFormat, this.selectedProfile!); + } + } + + get isDraftDisabled(): boolean | null { + return ( + !this.nameControl.valid || + this.fieldsHasError || + this.profileHasNoChanges() + ); + } + + profileHasNoChanges() { + const oldProfile = this.profile; + const newProfile = oldProfile + ? this.buildResponseFromForm( + oldProfile.status as ProfileStatus, + oldProfile + ) + : this.buildResponseFromForm('', oldProfile); + return ( + (oldProfile === null && this.profileIsEmpty(newProfile)) || + (oldProfile && this.compareProfiles(oldProfile, newProfile)) + ); + } + + private profileIsEmpty(profile: Profile) { + if (profile.name && profile.name !== '') { + return false; + } + + if (profile.questions) { + for (const question of profile.questions) { + if (this.isAnswerFilled(question)) { + return false; + } + } + } else { + return false; } + return true; } - get isDraftDisabled(): boolean { - return !this.nameControl.valid || this.fieldsHasError; + private isAnswerFilled(question: Question): boolean { + if ( + !question.answer || + (Array.isArray(question.answer) && question.answer.length === 0) + ) { + return false; + } + + if (typeof question.answer === 'string') { + return ( + question.answer.trim() !== '' && question.answer !== question.default + ); + } + + if (Array.isArray(question.answer)) { + if (!Array.isArray(question.default) && question.answer.length === 0) { + return true; + } + + return ( + question.answer.length > 0 && + JSON.stringify(question.answer) !== JSON.stringify(question.default) + ); + } + + return true; + } + + private compareProfiles(profile1: Profile, profile2: Profile) { + if (profile1.name !== profile2.name) { + return false; + } + if ( + (!profile1.rename && + profile2.rename && + profile2.rename !== profile1.name) || + (profile1.rename && + profile2.rename && + profile1.rename !== profile2.rename) + ) { + return false; + } + + if (profile1.status !== profile2.status) { + return false; + } + + for (const question of profile1.questions) { + const answer1 = question.answer; + const answer2 = profile2.questions?.find( + question2 => question2.question === question.question + )?.answer; + if (answer1 !== undefined && answer2 !== undefined) { + if (typeof question.answer === 'string') { + if (answer1 !== answer2) { + return false; + } + } else { + //the type of answer is array + if (answer1?.length !== answer2?.length) { + return false; + } + if ( + (answer1 as number[]).some( + answer => !(answer2 as number[]).includes(answer) + ) + ) + return false; + } + } else { + return !!answer1 == !!answer2; + } + } + return true; } private get fieldsHasError(): boolean { @@ -138,7 +270,7 @@ export class ProfileFormComponent implements OnInit { return this.profileForm.get(name.toString()) as AbstractControl; } - createProfileForm(questions: ProfileFormat[]): FormGroup { + createProfileForm(): FormGroup { // eslint-disable-next-line @typescript-eslint/no-explicit-any const group: any = {}; @@ -149,131 +281,116 @@ export class ProfileFormComponent implements OnInit { group['name'] = new FormControl('', [ this.profileValidators.textRequired(), - this.deviceValidators.deviceStringFormat(), + this.profileValidators.profileNameFormat(), this.nameValidator, ]); - questions.forEach((question, index) => { - if (question.type === FormControlType.SELECT_MULTIPLE) { - group[index] = this.getMultiSelectGroup(question); - } else { - const validators = this.getValidators( - question.type, - question.validation - ); - group[index] = new FormControl(question.default || '', validators); - } - }); return new FormGroup(group); } - getValidators(type: FormControlType, validation?: Validation): ValidatorFn[] { - const validators: ValidatorFn[] = []; - if (validation) { - if (validation.required) { - validators.push(this.profileValidators.textRequired()); - } - if (validation.max) { - validators.push(Validators.maxLength(Number(validation.max))); - } - if (type === FormControlType.EMAIL_MULTIPLE) { - validators.push(this.profileValidators.emailStringFormat()); - } - if (type === FormControlType.TEXT || type === FormControlType.TEXTAREA) { - validators.push(this.profileValidators.textFormat()); - } - } - return validators; - } - - getMultiSelectGroup(question: ProfileFormat): FormGroup { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const group: any = {}; - question.options?.forEach((option, index) => { - group[index] = false; - }); - return this.fb.group(group, { - validators: question.validation?.required - ? [this.profileValidators.multiSelectRequired] - : [], - }); - } - getFormGroup(name: string | number): FormGroup { return this.profileForm?.controls[name] as FormGroup; } fillProfileForm(profileFormat: ProfileFormat[], profile: Profile): void { - this.nameControl.setValue(profile.name); + const profileName = profile.rename ? profile.rename : profile.name; + this.nameControl.setValue(profileName); profileFormat.forEach((question, index) => { + const answer = profile.questions.find( + answers => answers.question === question.question + ); if (question.type === FormControlType.SELECT_MULTIPLE) { question.options?.forEach((item, idx) => { - if ((profile.questions[index].answer as number[])?.includes(idx)) { + if ((answer?.answer as number[])?.includes(idx)) { this.getFormGroup(index).controls[idx].setValue(true); } else { this.getFormGroup(index).controls[idx].setValue(false); } }); } else { - this.getControl(index).setValue(profile.questions[index].answer); + this.getControl(index).setValue(answer?.answer || ''); } }); + this.nameControl.markAsTouched(); this.triggerResize(); } onSaveClick(status: ProfileStatus) { - const response = this.buildResponseFromForm( - this.profileFormat, - this.profileForm, - status, - this.selectedProfile - ); + const response = this.buildResponseFromForm(status, this.selectedProfile); this.saveProfile.emit(response); + this.changeProfile = true; } - public markSectionAsDirty( - optionIndex: number, - optionLength: number, - formControlName: string - ) { - if (optionIndex === optionLength - 1) { - this.getControl(formControlName).markAsDirty(); + onDiscardClick() { + this.discard.emit(this.selectedProfile!); + } + + close(): Observable { + if (this.profileHasNoChanges() || this.profileForm.pristine) { + return of(true); } + return this.openCloseDialog().pipe(map(res => !!res)); + } + + openCloseDialog() { + const dialogRef = this.dialog.open(SimpleDialogComponent, { + ariaLabel: 'Discard the Risk Assessment changes', + data: { + title: 'Discard changes?', + content: `You have unsaved changes that would be permanently lost.`, + confirmName: 'Discard', + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: ['simple-dialog', 'discard-dialog'], + }); + + return dialogRef?.afterClosed(); + } + + private openCloseDialogToChangeProfile(profile: Profile | null) { + this.openCloseDialog().subscribe(close => { + if (close) { + this.changeProfile = true; + this.store.updateSelectedProfile(profile); + } + }); } private buildResponseFromForm( - initialQuestions: ProfileFormat[], - profileForm: FormGroup, - status: ProfileStatus, + status: ProfileStatus | '', profile: Profile | null ): Profile { // eslint-disable-next-line @typescript-eslint/no-explicit-any const request: any = { questions: [], }; - if (profile) { + if (profile && !this.isCopyProfile) { request.name = profile.name; - request.rename = this.nameControl.value?.trim(); + request.rename = this.nameControl?.value?.trim(); } else { - request.name = this.nameControl.value?.trim(); + request.name = this.nameControl?.value?.trim(); } const questions: Question[] = []; - initialQuestions.forEach((initialQuestion, index) => { + this.profileFormat?.forEach((initialQuestion, index) => { const question: Question = {}; question.question = initialQuestion.question; - + if (initialQuestion.default) { + question.default = initialQuestion.default; + } if (initialQuestion.type === FormControlType.SELECT_MULTIPLE) { const answer: number[] = []; initialQuestion.options?.forEach((_, idx) => { - const value = profileForm.value[index][idx]; + const value = this.profileForm.value[index][idx]; if (value) { answer.push(idx); } }); question.answer = answer; } else { - question.answer = profileForm.value[index]?.trim(); + question.answer = this.profileForm.value[index]?.trim() || ''; } questions.push(question); }); @@ -286,7 +403,7 @@ export class ProfileFormComponent implements OnInit { // Wait for content to render, then trigger textarea resize. afterNextRender( () => { - this.autosize?.forEach(item => item.resizeToFitContent(true)); + this.autosize()?.forEach(item => item.resizeToFitContent(true)); }, { injector: this.injector, @@ -294,11 +411,11 @@ export class ProfileFormComponent implements OnInit { ); } - private updateNameValidator() { + private updateNameValidator(profile: Profile) { this.nameControl.removeValidators([this.nameValidator]); this.nameValidator = this.profileValidators.differentProfileName( this.profileList, - this.profile + profile ); this.nameControl.addValidators(this.nameValidator); this.nameControl.updateValueAndValidity(); diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts index dcad4b397..2e742b39d 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts @@ -31,14 +31,30 @@ export class ProfileValidators { readonly STRING_FORMAT_REGEXP = new RegExp('^[^"\\\\]*$', 'u'); + // Not allowed symbols: <>?/:;@'"][=^!\#$%&*+{}|() + readonly PROFILE_NAME_FORMAT_REGEXP = new RegExp( + '^([^<>?:;@\'\\\\"\\[\\]=^!/,.#$%&*+{}|()]{1,28})$', + 'u' + ); + public differentProfileName( profiles: Profile[], profile: Profile | null ): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - const value = control.value?.trim(); - if (value && profiles.length && (!profile || profile?.name !== value)) { - const isSameProfileName = this.hasSameProfileName(value, profiles); + const value = control.value?.trim().toLowerCase(); + if ( + value && + profiles.length && + (!profile || + !profile.created || + (profile.created && profile?.name.toLowerCase() !== value)) + ) { + const isSameProfileName = this.hasSameProfileName( + value, + profiles, + profile?.created + ); return isSameProfileName ? { has_same_profile_name: true } : null; } return null; @@ -54,6 +70,10 @@ export class ProfileValidators { }; } + public profileNameFormat(): ValidatorFn { + return this.stringFormat(this.PROFILE_NAME_FORMAT_REGEXP); + } + public multiSelectRequired(g: FormGroup) { if (Object.values(g.value).every(value => value === false)) { return { required: true }; @@ -82,10 +102,15 @@ export class ProfileValidators { private hasSameProfileName( profileName: string, - profiles: Profile[] + profiles: Profile[], + created?: string ): boolean { return ( - profiles.some(profile => profile.name === profileName?.trim()) || false + profiles.some( + profile => + profile.name.toLowerCase() === profileName && + profile.created !== created + ) || false ); } } diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html index 35850f0ed..90d66d06a 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html @@ -13,17 +13,32 @@ See the License for the specific language governing permissions and limitations under the License. --> -
+
+ (keydown.enter)="enterProfileItem(profile)" + (keydown.space)="enterProfileItem(profile)"> + [attr.aria-label]=" + profile.status === ProfileStatus.EXPIRED + ? EXPIRED_TOOLTIP + : profile.status + "> + + error + -

+ {{ profile.name }} +

+
{{ profile.risk }} risk -

-

- {{ profile.name }} -

+

- {{ profile.created | date: 'dd MMM yyyy' }} + + Outdated ({{ profile.created | date: 'dd MMM yyyy' }}) + + + {{ profile.created | date: 'dd MMM yyyy' }} +

-
diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss index a9a22b9e4..509ef9d2c 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss @@ -13,28 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import 'src/theming/colors'; -@import 'src/theming/variables'; +@use 'colors'; +@use 'variables'; $profile-draft-icon-size: 22px; $profile-icon-container-size: 24px; -$profile-item-container-gap: 16px; - -:host.selected { - .profile-item-container { - background-color: $grey-100; - } -} +$profile-item-container-gap: 8px; .profile-item-container { + width: 100%; + height: 100%; display: grid; - grid-template-columns: minmax(160px, 1fr) $profile-icon-container-size; gap: $profile-item-container-gap; box-sizing: border-box; - padding: 12px 16px; - border-bottom: 1px solid $lighter-grey; align-items: center; - height: 92px; + &-expired { + grid-template-columns: minmax(160px, 1fr) $profile-icon-container-size; + } +} + +:host:has(.profile-item-container-expired) { + cursor: not-allowed; +} + +.profile-item-container-expired { + pointer-events: none; + opacity: 0.5; + .profile-item-info { + .profile-item-icon, + .profile-item-name, + .profile-item-created { + color: colors.$red-800; + } + } } .profile-item-icon-container { @@ -42,47 +53,55 @@ $profile-item-container-gap: 16px; display: inline-block; width: $profile-draft-icon-size; height: $profile-draft-icon-size; - padding: 2px; + padding-right: 16px; } -.profile-item-icon { - color: $grey-700; +.profile-item-icon, +.profile-draft-icon { + color: colors.$grey-800; } .profile-item-info { cursor: pointer; display: grid; - grid-template-columns: $profile-icon-container-size 1fr; + grid-template-columns: $profile-icon-container-size min-content auto; grid-template-areas: - 'icon .' - 'icon .' - 'icon .'; + 'icon . .' + 'icon created created'; column-gap: $profile-item-container-gap; align-items: center; p { - margin: 0; - font-family: $font-secondary, sans-serif; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - } - - ::ng-deep p.profile-item-risk { - margin-top: 6px; - justify-self: start; + margin: 0; } .profile-item-name { + max-width: 170px; font-size: 16px; - color: $grey-800; - min-height: 24px; + color: colors.$on-surface; + line-height: 24px; + padding-left: 16px; + justify-self: start; + align-self: end; + font-weight: 500; } .profile-item-created { + grid-area: created; font-size: 14px; - color: $grey-700; - min-height: 20px; + color: colors.$on-surface-variant; + line-height: 20px; + padding-left: 16px; + align-self: start; + justify-self: start; + } + + .profile-item-risk { + justify-self: start; + align-self: center; } } @@ -91,7 +110,7 @@ $profile-item-container-gap: 16px; height: 24px; width: 24px; border-radius: 4px; - color: $grey-700; + color: colors.$grey-700; display: flex; align-items: flex-start; justify-content: center; diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts index ae48e64ec..695786399 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts @@ -13,11 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; import { ProfileItemComponent } from './profile-item.component'; -import { PROFILE_MOCK } from '../../../mocks/profile.mock'; +import { + EXPIRED_PROFILE_MOCK, + PROFILE_MOCK, +} from '../../../mocks/profile.mock'; import { TestRunService } from '../../../services/test-run.service'; +import { LiveAnnouncer } from '@angular/cdk/a11y'; describe('ProfileItemComponent', () => { let component: ProfileItemComponent; @@ -25,11 +34,16 @@ describe('ProfileItemComponent', () => { let compiled: HTMLElement; const testRunServiceMock = jasmine.createSpyObj(['getRiskClass']); - + const mockLiveAnnouncer = jasmine.createSpyObj('mockLiveAnnouncer', [ + 'announce', + ]); beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ProfileItemComponent], - providers: [{ provide: TestRunService, useValue: testRunServiceMock }], + providers: [ + { provide: TestRunService, useValue: testRunServiceMock }, + { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, + ], }).compileComponents(); fixture = TestBed.createComponent(ProfileItemComponent); @@ -55,33 +69,58 @@ describe('ProfileItemComponent', () => { expect(date?.textContent?.trim()).toEqual('23 May 2024'); }); - it('should have profile name as part of buttons aria-label', () => { - const deleteButton = fixture.nativeElement.querySelector( - '.profile-item-button.delete' - ); + it('should emit click event on profile name clicked', () => { + const profileClickedSpy = spyOn(component.profileClicked, 'emit'); + const profileName = fixture.nativeElement.querySelector( + '.profile-item-name' + ) as HTMLElement; + + profileName.click(); - expect(deleteButton?.ariaLabel?.trim()).toContain(PROFILE_MOCK.name); + expect(profileClickedSpy).toHaveBeenCalledWith(PROFILE_MOCK); }); - it('should emit delete event on delete button clicked', () => { - const deleteSpy = spyOn(component.deleteButtonClicked, 'emit'); - const deleteButton = fixture.nativeElement.querySelector( - '.profile-item-button.delete' - ) as HTMLButtonElement; + it('should change tooltip on focusout', fakeAsync(() => { + component.profile = EXPIRED_PROFILE_MOCK; + fixture.detectChanges(); - deleteButton.click(); + fixture.nativeElement.dispatchEvent(new Event('focusout')); + tick(); - expect(deleteSpy).toHaveBeenCalledWith(PROFILE_MOCK.name); + expect(component.tooltip().message).toEqual( + 'Expired. Please, create a new Risk profile.' + ); + })); + + it('#getRiskClass should call getRiskClass on testRunService', () => { + const MOCK_RISK = 'mock value'; + component.getRiskClass(MOCK_RISK); + expect(testRunServiceMock.getRiskClass).toHaveBeenCalledWith(MOCK_RISK); }); - it('should emit click event on profile name clicked', () => { + it('#enterProfileItem should emit profileClicked', () => { const profileClickedSpy = spyOn(component.profileClicked, 'emit'); - const profileName = fixture.nativeElement.querySelector( - '.profile-item-name' - ) as HTMLElement; - profileName.click(); + component.enterProfileItem(PROFILE_MOCK); - expect(profileClickedSpy).toHaveBeenCalledWith(PROFILE_MOCK); + expect(profileClickedSpy).toHaveBeenCalled(); + }); + + describe('with Expired profile', () => { + beforeEach(() => { + component.enterProfileItem(EXPIRED_PROFILE_MOCK); + }); + + it('should change tooltip on enterProfileItem', () => { + expect(component.tooltip().message).toEqual( + 'This risk profile is outdated. Please create a new risk profile.' + ); + }); + + it('should announce', () => { + expect(mockLiveAnnouncer.announce).toHaveBeenCalledWith( + 'This risk profile is outdated. Please create a new risk profile.' + ); + }); }); }); diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts index 79bd08833..7eacc2205 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts @@ -17,8 +17,11 @@ import { ChangeDetectionStrategy, Component, EventEmitter, + HostListener, Input, Output, + viewChild, + inject, } from '@angular/core'; import { Profile, @@ -27,27 +30,59 @@ import { } from '../../../model/profile'; import { MatIcon } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; -import { CommonModule } from '@angular/common'; +import { CommonModule, DatePipe } from '@angular/common'; import { TestRunService } from '../../../services/test-run.service'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip'; +import { LiveAnnouncer } from '@angular/cdk/a11y'; @Component({ selector: 'app-profile-item', - standalone: true, + imports: [MatIcon, MatButtonModule, CommonModule, MatTooltipModule], + providers: [MatTooltip, DatePipe], templateUrl: './profile-item.component.html', styleUrl: './profile-item.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProfileItemComponent { + private readonly testRunService = inject(TestRunService); + private liveAnnouncer = inject(LiveAnnouncer); + private datePipe = inject(DatePipe); + public readonly ProfileStatus = ProfileStatus; + public readonly EXPIRED_TOOLTIP = + 'Expired. Please, create a new Risk profile.'; @Input() profile!: Profile; - @Output() deleteButtonClicked = new EventEmitter(); @Output() profileClicked = new EventEmitter(); - constructor(private readonly testRunService: TestRunService) {} + readonly tooltip = viewChild.required('tooltip'); + + @HostListener('focusout', ['$event']) + outEvent(): void { + if (this.profile.status === ProfileStatus.EXPIRED) { + this.tooltip().message = this.EXPIRED_TOOLTIP; + } + } public getRiskClass(riskResult: string): RiskResultClassName { return this.testRunService.getRiskClass(riskResult); } + + public async enterProfileItem(profile: Profile) { + if (profile.status === ProfileStatus.EXPIRED) { + const tooltip = this.tooltip(); + tooltip.message = + 'This risk profile is outdated. Please create a new risk profile.'; + tooltip.show(); + await this.liveAnnouncer.announce( + 'This risk profile is outdated. Please create a new risk profile.' + ); + } else { + this.profileClicked.emit(profile); + } + } + + getProfileItemLabel(profile: Profile) { + return `${profile.status} ${profile.risk} risk ${profile.name} ${this.datePipe.transform(profile.created, 'dd MMM yyyy')}`; + } } diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html index 2f11ea76b..502b7ad71 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html @@ -14,59 +14,68 @@ limitations under the License. --> - - - - -

Risk assessment

-
-
- -
-
-
-
- -
-

Saved profiles

-
-
- - -
-
+ + + + + + + + -
- -
+ + + +
+ + + + + + + +
diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss index c4ef49782..817e7ceb0 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss @@ -13,73 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import 'src/theming/colors'; -@import 'src/theming/variables'; +@use 'colors'; +@use 'variables'; +@use 'mixins'; :host { - overflow: hidden; - display: flex; + overflow: auto; } -.risk-assessment-content-empty { - height: 100%; - width: calc(100%); - display: flex; - align-items: center; - justify-content: center; -} - -:host:has(.profiles-drawer) { - .risk-assessment-content-empty { - width: calc(100% - $profiles-drawer-width); - } -} - -.risk-assessment-container, -.risk-assessment-content { - background-color: $white; -} - -.risk-assessment-container { - flex: 1; -} - -.risk-assessment-content { - display: flex; - flex-direction: column; - gap: 14px; - box-sizing: border-box; - padding-right: 94px; - overflow: hidden; +.risk-assessment-add-button { + @include mixins.add-button; } -.risk-assessment-toolbar { - height: 74px; - padding: 24px 0 8px 32px; - background: $white; -} - -.main-content { - padding: 16px 32px; - overflow: scroll; - width: calc(100% - $profiles-drawer-width); -} - -.profiles-drawer { - width: $profiles-drawer-width; - box-shadow: none; - border-left: 1px solid $light-grey; -} - -.profiles-drawer-header { - padding: 12px 12px 16px 24px; -} - -.profiles-drawer-header-title { - margin: 0; - font-size: 22px; - font-style: normal; - font-weight: 400; - line-height: 28px; - color: $dark-grey; +.risk-assessment-content-empty { + @include mixins.content-empty; } diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts index e2aa6332e..699a550a7 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts @@ -26,14 +26,23 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TestRunService } from '../../services/test-run.service'; import SpyObj = jasmine.SpyObj; import { MatSidenavModule } from '@angular/material/sidenav'; -import { NEW_PROFILE_MOCK, PROFILE_MOCK } from '../../mocks/profile.mock'; -import { of } from 'rxjs'; -import { Component, Input } from '@angular/core'; -import { Profile, ProfileFormat } from '../../model/profile'; +import { + DRAFT_COPY_PROFILE_MOCK, + NEW_PROFILE_MOCK, + NEW_PROFILE_MOCK_DRAFT, + PROFILE_MOCK, +} from '../../mocks/profile.mock'; +import { of, Subscription } from 'rxjs'; +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { Profile, ProfileAction, ProfileFormat } from '../../model/profile'; import { MatDialogRef } from '@angular/material/dialog'; import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; import { RiskAssessmentStore } from './risk-assessment.store'; import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { Observable } from 'rxjs/internal/Observable'; +import { ProfileFormComponent } from './profile-form/profile-form.component'; +import { MatIcon } from '@angular/material/icon'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; describe('RiskAssessmentComponent', () => { let component: RiskAssessmentComponent; @@ -43,6 +52,7 @@ describe('RiskAssessmentComponent', () => { const mockLiveAnnouncer: SpyObj = jasmine.createSpyObj([ 'announce', + 'clear', ]); let compiled: HTMLElement; @@ -59,21 +69,34 @@ describe('RiskAssessmentComponent', () => { 'setFocusOnCreateButton', 'setFocusOnSelectedProfile', 'setFocusOnProfileForm', + 'updateProfiles', + 'removeProfile', + 'isOpenCreateProfile$', + 'profileFormat$', ]); + mockRiskAssessmentStore.profileFormat$ = of([]); + await TestBed.configureTestingModule({ - declarations: [ + declarations: [FakeProfileItemComponent, FakeProfileFormComponent], + imports: [ RiskAssessmentComponent, - FakeProfileItemComponent, - FakeProfileFormComponent, + MatToolbarModule, + MatSidenavModule, + BrowserAnimationsModule, + MatIconTestingModule, + MatIcon, ], - imports: [MatToolbarModule, MatSidenavModule, BrowserAnimationsModule], providers: [ { provide: TestRunService, useValue: mockService }, { provide: RiskAssessmentStore, useValue: mockRiskAssessmentStore }, { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, ], - }).compileComponents(); + }) + .overrideComponent(RiskAssessmentComponent, { + set: { encapsulation: ViewEncapsulation.None }, + }) + .compileComponents(); TestBed.overrideProvider(RiskAssessmentStore, { useValue: mockRiskAssessmentStore, @@ -88,18 +111,54 @@ describe('RiskAssessmentComponent', () => { expect(component).toBeTruthy(); }); - describe('with no data', () => { + it('should open form if isOpenAddDevice$ as true', () => { + mockRiskAssessmentStore.profileFormat$ = of([], []); + mockRiskAssessmentStore.isOpenCreateProfile$ = of(true); + component.ngOnInit(); + + expect(component.isOpenProfileForm).toBeTrue(); + }); + + describe('with no profiles data', () => { beforeEach(() => { component.viewModel$ = of({ profiles: [] as Profile[], profileFormat: [], selectedProfile: null, + actions: [ + { action: ProfileAction.Copy, icon: 'content_copy' }, + { action: ProfileAction.Delete, icon: 'delete' }, + ], }); mockRiskAssessmentStore.profiles$ = of([]); fixture.detectChanges(); }); - it('should have "New Risk Assessment" button', () => { + it('should have title', () => { + const title = compiled.querySelector('h2.title'); + const titleContent = title?.innerHTML.trim(); + + expect(title).toBeTruthy(); + expect(titleContent).toContain('Risk Assessment'); + }); + + it('should have empty page with necessary content', () => { + const emptyHeader = compiled.querySelector( + 'app-empty-page .empty-message-header' + ); + const emptyMessage = compiled.querySelector( + 'app-empty-page .empty-message-main' + ); + + expect(emptyHeader).toBeTruthy(); + expect(emptyHeader?.innerHTML).toContain('Risk assessment'); + expect(emptyMessage).toBeTruthy(); + expect(emptyMessage?.innerHTML).toContain( + 'complete a brief risk questionnaire' + ); + }); + + it('should have "Create Risk Profile" button', () => { const newRiskAssessmentBtn = compiled.querySelector( '.risk-assessment-add-button' ); @@ -115,22 +174,14 @@ describe('RiskAssessmentComponent', () => { newRiskAssessmentBtn.click(); fixture.detectChanges(); - const toolbarEl = compiled.querySelector('.risk-assessment-toolbar'); const title = compiled.querySelector('h2.title'); const titleContent = title?.innerHTML.trim(); const profileForm = compiled.querySelectorAll('app-profile-form'); - expect(toolbarEl).not.toBeNull(); expect(title).toBeTruthy(); - expect(titleContent).toContain('Risk assessment'); + expect(titleContent).toContain('Risk Assessment'); expect(profileForm).toBeTruthy(); }); - - it('should not have profiles drawer', () => { - const profilesDrawer = compiled.querySelector('.profiles-drawer'); - - expect(profilesDrawer).toBeFalsy(); - }); }); describe('with profiles data', () => { @@ -139,16 +190,14 @@ describe('RiskAssessmentComponent', () => { profiles: [PROFILE_MOCK, PROFILE_MOCK], profileFormat: [], selectedProfile: null, + actions: [ + { action: ProfileAction.Copy, icon: 'content_copy' }, + { action: ProfileAction.Delete, icon: 'delete' }, + ], }); fixture.detectChanges(); }); - it('should have profiles drawer', () => { - const profilesDrawer = compiled.querySelector('.profiles-drawer'); - - expect(profilesDrawer).toBeTruthy(); - }); - it('should have profile items', () => { const profileItems = compiled.querySelectorAll('app-profile-item'); @@ -162,19 +211,18 @@ describe('RiskAssessmentComponent', () => { } as MatDialogRef); tick(); - component.deleteProfile(PROFILE_MOCK.name, 0, null); + component.deleteProfile(PROFILE_MOCK, [PROFILE_MOCK], PROFILE_MOCK); tick(); expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, { - ariaLabel: 'Delete risk profile', data: { title: 'Delete risk profile?', content: `You are about to delete ${PROFILE_MOCK.name}. Are you sure?`, }, - autoFocus: true, + autoFocus: 'dialog', hasBackdrop: true, disableClose: true, - panelClass: 'simple-dialog', + panelClass: ['simple-dialog', 'delete-dialog'], }); openSpy.calls.reset(); @@ -184,11 +232,43 @@ describe('RiskAssessmentComponent', () => { spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), } as MatDialogRef); + + mockRiskAssessmentStore.deleteProfile.and.callFake( + ( + observableOrValue: + | { name: string; onDelete: (idx: number) => void } + | Observable<{ name: string; onDelete: (idx: number) => void }> + ) => { + // @ts-expect-error onDelete exist in object + observableOrValue?.onDelete(1); + return new Subscription(); + } + ); + + tick(); + + component.deleteProfile(PROFILE_MOCK, [PROFILE_MOCK], PROFILE_MOCK); tick(); - component.deleteProfile(PROFILE_MOCK.name, 0, PROFILE_MOCK); + expect( + mockRiskAssessmentStore.updateSelectedProfile + ).toHaveBeenCalledWith(null); + expect(component.isOpenProfileForm).toBeFalse(); + })); + + it('should remove copy and close form when unsaved copy is deleted', fakeAsync(() => { + spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of(true), + } as MatDialogRef); + component.isCopyProfile = true; + component.deleteProfile( + DRAFT_COPY_PROFILE_MOCK, + [DRAFT_COPY_PROFILE_MOCK, PROFILE_MOCK], + DRAFT_COPY_PROFILE_MOCK + ); tick(); + expect(mockRiskAssessmentStore.removeProfile).toHaveBeenCalled(); expect( mockRiskAssessmentStore.updateSelectedProfile ).toHaveBeenCalledWith(null); @@ -217,6 +297,34 @@ describe('RiskAssessmentComponent', () => { }); }); + describe('#getCopyOfProfile', () => { + it('should open the form with copy of profile', () => { + const copy = component.getCopyOfProfile(PROFILE_MOCK); + expect(copy).toEqual(DRAFT_COPY_PROFILE_MOCK); + }); + }); + + it('#profileClicked should call openForm with profile', fakeAsync(() => { + spyOn(component, 'openForm'); + + component.profileClicked(PROFILE_MOCK); + tick(); + + expect(component.openForm).toHaveBeenCalledWith(PROFILE_MOCK); + })); + + it('#copyProfileAndOpenForm should call openForm with copy of profile', fakeAsync(() => { + spyOn(component, 'openForm'); + + component.copyProfileAndOpenForm(PROFILE_MOCK, [ + PROFILE_MOCK, + PROFILE_MOCK, + ]); + tick(); + + expect(component.openForm).toHaveBeenCalledWith(DRAFT_COPY_PROFILE_MOCK); + })); + describe('#saveProfile', () => { describe('with no profile selected', () => { beforeEach(() => { @@ -224,10 +332,13 @@ describe('RiskAssessmentComponent', () => { }); it('should call store saveProfile when it is new profile', () => { - expect(mockRiskAssessmentStore.saveProfile).toHaveBeenCalledWith({ + const args = mockRiskAssessmentStore.saveProfile.calls.argsFor(0); + // @ts-expect-error config is in object + expect(args[0].profile).toEqual({ name: 'test', questions: [], }); + expect(mockRiskAssessmentStore.saveProfile).toHaveBeenCalled(); }); it('should close the form', () => { @@ -236,7 +347,7 @@ describe('RiskAssessmentComponent', () => { }); describe('with profile selected', () => { - it('should open save profile modal', fakeAsync(() => { + it('should open save profile modal for valid profile', fakeAsync(() => { const openSpy = spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), } as MatDialogRef); @@ -244,22 +355,40 @@ describe('RiskAssessmentComponent', () => { component.saveProfileClicked(NEW_PROFILE_MOCK, PROFILE_MOCK); expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, { - ariaLabel: 'Save changes', data: { - title: 'Save changes', + title: 'Save profile', + content: `You are about to save changes in Primary profile. Are you sure?`, + }, + autoFocus: 'dialog', + hasBackdrop: true, + disableClose: true, + }); + + openSpy.calls.reset(); + })); + + it('should open save draft profile modal', fakeAsync(() => { + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of(true), + } as MatDialogRef); + + component.saveProfileClicked(NEW_PROFILE_MOCK_DRAFT, PROFILE_MOCK); + + expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, { + data: { + title: 'Save draft profile', content: `You are about to save changes in Primary profile. Are you sure?`, }, - autoFocus: true, + autoFocus: 'dialog', hasBackdrop: true, disableClose: true, - panelClass: 'simple-dialog', }); openSpy.calls.reset(); })); it('should call store saveProfile', fakeAsync(() => { - spyOn(component.dialog, 'open').and.returnValue({ + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), } as MatDialogRef); @@ -267,9 +396,23 @@ describe('RiskAssessmentComponent', () => { tick(); - expect(mockRiskAssessmentStore.saveProfile).toHaveBeenCalledWith( - NEW_PROFILE_MOCK - ); + const args = mockRiskAssessmentStore.saveProfile.calls.argsFor(0); + // @ts-expect-error config is in object + expect(args[0].profile).toEqual(NEW_PROFILE_MOCK); + expect(mockRiskAssessmentStore.saveProfile).toHaveBeenCalled(); + openSpy.calls.reset(); + })); + + it('should call store saveProfile and should not open save draft profile modal when profile does not have changes', fakeAsync(() => { + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of(true), + } as MatDialogRef); + + component.saveProfileClicked(PROFILE_MOCK, PROFILE_MOCK); + + expect(openSpy).not.toHaveBeenCalled(); + expect(mockRiskAssessmentStore.saveProfile).toHaveBeenCalled(); + openSpy.calls.reset(); })); it('should close the form', fakeAsync(() => { @@ -284,12 +427,68 @@ describe('RiskAssessmentComponent', () => { })); }); }); + + describe('#discard', () => { + beforeEach(async () => { + await component.openForm(); + }); + + it('should call openCloseDialog', () => { + const openCloseDialogSpy = spyOn( + component.form(), + 'openCloseDialog' + ).and.returnValue(of(true)); + + component.discard(null, []); + + expect(openCloseDialogSpy).toHaveBeenCalled(); + + openCloseDialogSpy.calls.reset(); + }); + + describe('after dialog closed with discard selected', () => { + beforeEach(() => { + spyOn( + component.form(), + 'openCloseDialog' + ).and.returnValue(of(true)); + component.discard(null, []); + }); + + it('should update selected profile', () => { + expect( + mockRiskAssessmentStore.updateSelectedProfile + ).toHaveBeenCalledWith(null); + }); + + it('should close the form', () => { + expect(component.isOpenProfileForm).toBeFalse(); + }); + }); + + describe('with selected copy profile', () => { + beforeEach(fakeAsync(() => { + spyOn( + component.form(), + 'openCloseDialog' + ).and.returnValue(of(true)); + component.isCopyProfile = true; + component.discard(DRAFT_COPY_PROFILE_MOCK, [DRAFT_COPY_PROFILE_MOCK]); + tick(100); + })); + + it('should remove copy if not saved', () => { + expect(mockRiskAssessmentStore.removeProfile).toHaveBeenCalled(); + }); + }); + }); }); }); @Component({ selector: 'app-profile-item', template: '
', + standalone: false, }) class FakeProfileItemComponent { @Input() profile!: Profile; @@ -298,9 +497,11 @@ class FakeProfileItemComponent { @Component({ selector: 'app-profile-form', template: '
', + standalone: false, }) class FakeProfileFormComponent { @Input() profiles!: Profile[]; + @Input() isCopyProfile!: boolean; @Input() selectedProfile!: Profile; @Input() profileFormat!: ProfileFormat[]; } diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts index 503d87a52..a306d0503 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts @@ -15,37 +15,122 @@ */ import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, + ElementRef, + inject, OnDestroy, OnInit, + viewChild, + ViewContainerRef, } from '@angular/core'; import { RiskAssessmentStore } from './risk-assessment.store'; import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; -import { Subject, takeUntil } from 'rxjs'; +import { + combineLatest, + Observable, + of, + skip, + Subject, + takeUntil, + timer, +} from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; import { LiveAnnouncer } from '@angular/cdk/a11y'; -import { Profile } from '../../model/profile'; -import { Observable } from 'rxjs/internal/Observable'; +import { Profile, ProfileAction, ProfileStatus } from '../../model/profile'; +import { DeviceValidators } from '../devices/components/device-form/device.validators'; +import { SuccessDialogComponent } from './components/success-dialog/success-dialog.component'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { ProfileItemComponent } from './profile-item/profile-item.component'; +import { + MAT_FORM_FIELD_DEFAULT_OPTIONS, + MatFormFieldDefaultOptions, +} from '@angular/material/form-field'; +import { CommonModule } from '@angular/common'; +import { MatInputModule } from '@angular/material/input'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { ProfileFormComponent } from './profile-form/profile-form.component'; +import { MatIconModule } from '@angular/material/icon'; +import { EmptyPageComponent } from '../../components/empty-page/empty-page.component'; +import { ListLayoutComponent } from '../../components/list-layout/list-layout.component'; +import { LayoutType } from '../../model/layout-type'; +import { NoEntitySelectedComponent } from '../../components/no-entity-selected/no-entity-selected.component'; +import { EntityAction, EntityActionResult } from '../../model/entity-action'; +import { CanComponentDeactivate } from '../../guards/can-deactivate.guard'; + +const matFormFieldDefaultOptions: MatFormFieldDefaultOptions = { + hideRequiredMarker: true, +}; @Component({ selector: 'app-risk-assessment', templateUrl: './risk-assessment.component.html', styleUrl: './risk-assessment.component.scss', - providers: [RiskAssessmentStore], + imports: [ + CommonModule, + MatToolbarModule, + MatButtonModule, + MatIconModule, + ReactiveFormsModule, + MatInputModule, + MatSidenavModule, + EmptyPageComponent, + ListLayoutComponent, + ProfileFormComponent, + ProfileItemComponent, + NoEntitySelectedComponent, + ], + providers: [ + RiskAssessmentStore, + { + provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, + useValue: matFormFieldDefaultOptions, + }, + ], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RiskAssessmentComponent implements OnInit, OnDestroy { +export class RiskAssessmentComponent + implements OnInit, OnDestroy, CanComponentDeactivate +{ + readonly LayoutType = LayoutType; + readonly ProfileStatus = ProfileStatus; + readonly form = viewChild('profileFormComponent'); + private store = inject(RiskAssessmentStore); + private liveAnnouncer = inject(LiveAnnouncer); + cd = inject(ChangeDetectorRef); + private elementRef = inject(ElementRef); + private destroy$: Subject = new Subject(); + dialog = inject(MatDialog); + element = inject(ViewContainerRef); + viewModel$ = this.store.viewModel$; isOpenProfileForm = false; - private destroy$: Subject = new Subject(); - constructor( - private store: RiskAssessmentStore, - public dialog: MatDialog, - private liveAnnouncer: LiveAnnouncer - ) {} + isCopyProfile = false; + + canDeactivate(): Observable { + const form = this.form(); + if (form) { + return form.close(); + } else { + return of(true); + } + } ngOnInit() { this.store.getProfilesFormat(); + + combineLatest([ + this.store.isOpenCreateProfile$, + this.store.profileFormat$.pipe(skip(1)), + ]) + .pipe(takeUntil(this.destroy$)) + .subscribe(([isOpenCreateProfile]) => { + if (isOpenCreateProfile) { + this.openForm(); + } + }); } ngOnDestroy() { @@ -53,28 +138,51 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { this.destroy$.unsubscribe(); } + async profileClicked(profile: Profile | null = null) { + if (profile === null || profile.status !== ProfileStatus.EXPIRED) { + await this.openForm(profile); + } + } + async openForm(profile: Profile | null = null) { this.isOpenProfileForm = true; this.store.updateSelectedProfile(profile); await this.liveAnnouncer.announce('Risk assessment questionnaire'); this.store.setFocusOnProfileForm(); + this.cd.detectChanges(); + } + + async copyProfileAndOpenForm(profile: Profile, profiles: Profile[]) { + this.isCopyProfile = true; + const copyOfProfile = this.getCopyOfProfile(profile); + this.store.updateProfiles([copyOfProfile, ...profiles]); + await this.openForm(copyOfProfile); + } + + getCopyOfProfile(profile: Profile): Profile { + const copyOfProfile = { ...profile }; + copyOfProfile.name = this.getCopiedProfileName(profile.name); + delete copyOfProfile.created; // new profile is not create yet + delete copyOfProfile.risk; + copyOfProfile.status = ProfileStatus.DRAFT; + return copyOfProfile; } deleteProfile( - profileName: string, - index: number, + profile: Profile, + profiles: Profile[], selectedProfile: Profile | null ): void { + const profileName = profile.name; const dialogRef = this.dialog.open(SimpleDialogComponent, { - ariaLabel: 'Delete risk profile', data: { title: 'Delete risk profile?', content: `You are about to delete ${profileName}. Are you sure?`, }, - autoFocus: true, + autoFocus: 'dialog', hasBackdrop: true, disableClose: true, - panelClass: 'simple-dialog', + panelClass: ['simple-dialog', 'delete-dialog'], }); dialogRef @@ -82,32 +190,189 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe(deleteProfile => { if (deleteProfile) { - this.store.deleteProfile(profileName); - this.closeFormAfterDelete(profileName, selectedProfile); - this.setFocus(index); + if ( + profile && + profile.status === ProfileStatus.DRAFT && + !profile.created + ) { + this.deleteCopy(profile, profiles); + this.closeFormAfterDelete(profile.name, selectedProfile); + this.focusAddButton(); + return; + } else { + this.store.deleteProfile({ + name: profileName, + onDelete: (idx = 0) => { + this.closeFormAfterDelete(profileName, selectedProfile); + timer(100).subscribe(() => { + this.setFocus(idx); + }); + }, + }); + } + } else { + this.store.setFocusOnSelectedProfile(); } }); } saveProfileClicked(profile: Profile, selectedProfile: Profile | null): void { + this.liveAnnouncer.clear(); if (!selectedProfile) { - this.saveProfile(profile); - this.store.setFocusOnCreateButton(); + this.saveProfile(profile, () => { + this.store.setFocusOnCreateButton(); + this.store.scrollToSelectedProfile(); + }); + } else if ( + this.compareProfiles(profile, selectedProfile) || + this.isCopyProfile + ) { + this.saveProfile(profile, this.store.setFocusOnSelectedProfile); } else { - this.openSaveDialog(selectedProfile.name) + this.openSaveDialog( + selectedProfile.name, + profile.status === ProfileStatus.DRAFT + ) .pipe(takeUntil(this.destroy$)) .subscribe(saveProfile => { if (saveProfile) { - this.saveProfile(profile); - this.store.setFocusOnSelectedProfile(); + this.saveProfile(profile, this.store.setFocusOnSelectedProfile); } }); } } - trackByIndex = (index: number): number => { - return index; - }; + discard(selectedProfile: Profile | null, profiles: Profile[]) { + this.liveAnnouncer.clear(); + this.openCloseDialog(selectedProfile, profiles); + } + + private openCloseDialog( + selectedProfile: Profile | null, + profiles: Profile[] + ) { + this.form() + ?.openCloseDialog() + .pipe(takeUntil(this.destroy$)) + .subscribe(close => { + if (close) { + if (selectedProfile && this.isCopyProfile) { + this.deleteCopy(selectedProfile, profiles); + } + this.isCopyProfile = false; + this.isOpenProfileForm = false; + this.store.updateSelectedProfile(null); + this.cd.markForCheck(); + timer(100).subscribe(() => { + this.focusSelectedButton(); + }); + } + }); + } + + private focusSelectedButton() { + const selectedButton = this.elementRef.nativeElement.querySelector( + 'app-profile-item.selected .profile-item-container' + ); + if (selectedButton) { + selectedButton.focus(); + } else { + this.focusAddButton(); + } + } + + private focusAddButton(): void { + const addButton = + this.elementRef.nativeElement.querySelector('.add-entity-button'); + addButton?.focus(); + } + + deleteCopy(copyOfProfile: Profile, profiles: Profile[]) { + this.isCopyProfile = false; + this.store.removeProfile(copyOfProfile.name, profiles); + } + + actions(actions: EntityAction[]) { + return (profile: Profile) => { + // expired profiles or unsaved copy of profile can only be removed + if ( + profile.status === ProfileStatus.EXPIRED || + (profile.status === ProfileStatus.DRAFT && !profile.created) + ) { + return [{ action: ProfileAction.Delete, icon: 'delete' }]; + } + return actions; + }; + } + + menuItemClicked( + { action, entity }: EntityActionResult, + profiles: Profile[], + selectedProfile: Profile | null + ) { + switch (action) { + case ProfileAction.Copy: + this.copyProfileAndOpenForm(entity, profiles); + break; + case ProfileAction.Delete: + this.deleteProfile(entity, profiles, selectedProfile); + break; + } + } + + private getCopiedProfileName(name: string): string { + name = `Copy of ${name}`; + if (name.length > DeviceValidators.STRING_FORMAT_MAX_LENGTH) { + name = + name.substring(0, DeviceValidators.STRING_FORMAT_MAX_LENGTH - 3) + + '...'; + } + return name; + } + + private compareProfiles(profile1: Profile, profile2: Profile) { + if (profile1.name !== profile2.name) { + return false; + } + if ( + profile1.rename && + (profile1.rename !== profile1.name || profile1.rename !== profile2.name) + ) { + return false; + } + if (profile1.status !== profile2.status) { + return false; + } + + for (const question of profile1.questions) { + const answer1 = question.answer; + const answer2 = profile2.questions?.find( + question2 => question2.question === question.question + )?.answer; + if (answer1 !== undefined && answer2 !== undefined) { + if (typeof question.answer === 'string') { + if (answer1 !== answer2) { + return false; + } + } else { + //the type of answer is array + if (answer1?.length !== answer2?.length) { + return false; + } + if ( + (answer1 as number[]).some( + answer => !(answer2 as number[]).includes(answer) + ) + ) + return false; + } + } else { + return !!answer1 == !!answer2; + } + } + + return true; + } private closeFormAfterDelete(name: string, selectedProfile: Profile | null) { if (selectedProfile?.name === name) { @@ -116,35 +381,67 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { } } - private saveProfile(profile: Profile) { - this.store.saveProfile(profile); - this.isOpenProfileForm = false; + private saveProfile(profile: Profile, focusElement: () => void) { + this.store.saveProfile({ + profile, + onSave: (profile: Profile) => { + if (profile.status === ProfileStatus.VALID) { + this.openSuccessDialog(profile, focusElement); + } else { + focusElement(); + } + this.store.updateSelectedProfile(profile); + }, + }); + this.isCopyProfile = false; } private setFocus(index: number): void { - const nextItem = window.document.querySelector( - `.profile-item-${index + 1}` - ) as HTMLElement; - const firstItem = window.document.querySelector( - `.profile-item-0` - ) as HTMLElement; + const nextItem = this.elementRef.nativeElement.querySelectorAll( + 'app-profile-item .profile-item-info' + )[index]; - this.store.setFocus({ nextItem, firstItem }); + if (nextItem) { + nextItem.focus(); + } else { + this.focusAddButton(); + } } - private openSaveDialog(profileName: string): Observable { + private openSaveDialog( + profileName: string, + draft: boolean = false + ): Observable { const dialogRef = this.dialog.open(SimpleDialogComponent, { - ariaLabel: 'Save changes', data: { - title: 'Save changes', + title: `Save ${draft ? 'draft profile' : 'profile'}`, content: `You are about to save changes in ${profileName}. Are you sure?`, }, + autoFocus: 'dialog', + hasBackdrop: true, + disableClose: true, + }); + + return dialogRef?.afterClosed(); + } + + private openSuccessDialog(profile: Profile, focusElement: () => void): void { + const dialogRef = this.dialog.open(SuccessDialogComponent, { + ariaLabel: 'Risk assessment completed', + data: { + profile, + }, autoFocus: true, hasBackdrop: true, disableClose: true, panelClass: 'simple-dialog', }); - return dialogRef?.afterClosed(); + dialogRef + ?.afterClosed() + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + focusElement(); + }); } } diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.module.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.module.ts deleted file mode 100644 index 97d48989f..000000000 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.module.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { RiskAssessmentRoutingModule } from './risk-assessment-routing.module'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { RiskAssessmentComponent } from './risk-assessment.component'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { ProfileItemComponent } from './profile-item/profile-item.component'; -import { MatButtonModule } from '@angular/material/button'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatInputModule } from '@angular/material/input'; -import { - MAT_FORM_FIELD_DEFAULT_OPTIONS, - MatFormFieldDefaultOptions, -} from '@angular/material/form-field'; -import { ProfileFormComponent } from './profile-form/profile-form.component'; - -const matFormFieldDefaultOptions: MatFormFieldDefaultOptions = { - hideRequiredMarker: true, -}; - -@NgModule({ - declarations: [RiskAssessmentComponent], - imports: [ - CommonModule, - RiskAssessmentRoutingModule, - MatToolbarModule, - MatButtonModule, - ReactiveFormsModule, - MatInputModule, - MatSidenavModule, - ProfileFormComponent, - ProfileItemComponent, - ], - providers: [ - { - provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, - useValue: matFormFieldDefaultOptions, - }, - ], -}) -export class RiskAssessmentModule {} diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts index 3bc123929..c7fb47741 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts @@ -29,7 +29,8 @@ import { import { FocusManagerService } from '../../services/focus-manager.service'; import { AppState } from '../../store/state'; import { selectRiskProfiles } from '../../store/selectors'; -import { fetchRiskProfiles, setRiskProfiles } from '../../store/actions'; +import { setRiskProfiles } from '../../store/actions'; +import { ProfileAction } from '../../model/profile'; describe('RiskAssessmentStore', () => { let riskAssessmentStore: RiskAssessmentStore; @@ -104,6 +105,10 @@ describe('RiskAssessmentStore', () => { profiles: [PROFILE_MOCK, PROFILE_MOCK_2], profileFormat: [], selectedProfile: null, + actions: [ + { action: ProfileAction.Copy, icon: 'content_copy' }, + { action: ProfileAction.Delete, icon: 'delete' }, + ], }); done(); }); @@ -115,7 +120,12 @@ describe('RiskAssessmentStore', () => { it('should dispatch setRiskProfiles', () => { mockService.deleteProfile.and.returnValue(of(true)); - riskAssessmentStore.deleteProfile(PROFILE_MOCK.name); + riskAssessmentStore.deleteProfile({ + name: PROFILE_MOCK.name, + onDelete: (idx: number) => { + return idx; + }, + }); expect(store.dispatch).toHaveBeenCalledWith( setRiskProfiles({ riskProfiles: [PROFILE_MOCK_2] }) @@ -128,33 +138,35 @@ describe('RiskAssessmentStore', () => { const mockFirstItem = document.createElement('section') as HTMLElement; const mockNullEL = window.document.querySelector(`.mock`) as HTMLElement; - it('should set focus to the next profile item when available', () => { + it('should set focus to the next profile item when available', fakeAsync(() => { const mockData = { nextItem: mockNextItem, firstItem: mockFirstItem, }; riskAssessmentStore.setFocus(mockData); + tick(100); expect( mockFocusManagerService.focusFirstElementInContainer ).toHaveBeenCalledWith(mockNextItem); - }); + })); - it('should set focus to the first profile item when available and no next item', () => { + it('should set focus to the first profile item when available and no next item', fakeAsync(() => { const mockData = { nextItem: mockNullEL, firstItem: mockFirstItem, }; riskAssessmentStore.setFocus(mockData); + tick(100); expect( mockFocusManagerService.focusFirstElementInContainer ).toHaveBeenCalledWith(mockFirstItem); - }); + })); - it('should set focus to the first element in the main when no items', () => { + it('should set focus to the first element in the main when no items', fakeAsync(() => { const mockData = { nextItem: mockNullEL, firstItem: mockFirstItem, @@ -164,17 +176,16 @@ describe('RiskAssessmentStore', () => { store.refreshState(); riskAssessmentStore.setFocus(mockData); + tick(100); expect( mockFocusManagerService.focusFirstElementInContainer ).toHaveBeenCalledWith(); - }); + })); }); describe('setFocusOnCreateButton', () => { - const container = document.createElement('div') as HTMLElement; - container.classList.add('risk-assessment-content-empty'); - document.querySelector('body')?.appendChild(container); + const container = window.document.querySelector('app-risk-assessment'); it('should call focusFirstElementInContainer', fakeAsync(() => { riskAssessmentStore.setFocusOnCreateButton(); @@ -186,21 +197,28 @@ describe('RiskAssessmentStore', () => { })); }); - describe('setFocusOnSelectedProfile', () => { + describe('with selected profile', () => { const container = document.createElement('div') as HTMLElement; - container.classList.add('profiles-drawer-content'); + container.classList.add('entity-list'); const inner = document.createElement('div') as HTMLElement; inner.classList.add('selected'); container.appendChild(inner); document.querySelector('body')?.appendChild(container); - it('should call focusFirstElementInContainer', () => { + it('setFocusOnSelectedProfile should call focusFirstElementInContainer', () => { riskAssessmentStore.setFocusOnSelectedProfile(); expect( mockFocusManagerService.focusFirstElementInContainer ).toHaveBeenCalledWith(inner); }); + + it('scrollToSelectedProfile should call focusFirstElementInContainer', () => { + const scrollSpy = spyOn(inner, 'scrollIntoView'); + riskAssessmentStore.scrollToSelectedProfile(); + + expect(scrollSpy).toHaveBeenCalled(); + }); }); describe('setFocusOnProfileForm', () => { @@ -230,10 +248,18 @@ describe('RiskAssessmentStore', () => { }); describe('saveProfile', () => { - it('should dispatch fetchRiskProfiles', () => { - riskAssessmentStore.saveProfile(NEW_PROFILE_MOCK); + it('should dispatch setRiskProfiles', () => { + const onSave = jasmine.createSpy('onSave'); + mockService.fetchProfiles.and.returnValue(of([NEW_PROFILE_MOCK])); + riskAssessmentStore.saveProfile({ + profile: NEW_PROFILE_MOCK, + onSave, + }); - expect(store.dispatch).toHaveBeenCalledWith(fetchRiskProfiles()); + expect(store.dispatch).toHaveBeenCalledWith( + setRiskProfiles({ riskProfiles: [NEW_PROFILE_MOCK] }) + ); + expect(onSave).toHaveBeenCalled(); }); }); }); diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts index 93e89b434..94833ddf0 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts @@ -14,33 +14,44 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { ComponentStore } from '@ngrx/component-store'; import { tap, withLatestFrom } from 'rxjs/operators'; -import { delay, exhaustMap } from 'rxjs'; +import { catchError, delay, EMPTY, exhaustMap, throwError, timer } from 'rxjs'; import { TestRunService } from '../../services/test-run.service'; -import { Profile, ProfileFormat } from '../../model/profile'; +import { Profile, ProfileAction, ProfileFormat } from '../../model/profile'; import { FocusManagerService } from '../../services/focus-manager.service'; import { Store } from '@ngrx/store'; import { AppState } from '../../store/state'; -import { selectRiskProfiles } from '../../store/selectors'; -import { fetchRiskProfiles, setRiskProfiles } from '../../store/actions'; +import { + selectIsOpenCreateProfile, + selectRiskProfiles, +} from '../../store/selectors'; +import { setRiskProfiles } from '../../store/actions'; +import { EntityAction } from '../../model/entity-action'; export interface AppComponentState { selectedProfile: Profile | null; profiles: Profile[]; profileFormat: ProfileFormat[]; + actions: EntityAction[]; } @Injectable() export class RiskAssessmentStore extends ComponentStore { + private testRunService = inject(TestRunService); + private store = inject>(Store); + private focusManagerService = inject(FocusManagerService); + profiles$ = this.store.select(selectRiskProfiles); profileFormat$ = this.select(state => state.profileFormat); selectedProfile$ = this.select(state => state.selectedProfile); - + actions$ = this.select(state => state.actions); + isOpenCreateProfile$ = this.store.select(selectIsOpenCreateProfile); viewModel$ = this.select({ profiles: this.profiles$, profileFormat: this.profileFormat$, selectedProfile: this.selectedProfile$, + actions: this.actions$, }); updateProfileFormat = this.updater( @@ -56,14 +67,19 @@ export class RiskAssessmentStore extends ComponentStore { }) ); - deleteProfile = this.effect(trigger$ => { + deleteProfile = this.effect<{ + name: string; + onDelete: (idx: number) => void; + }>(trigger$ => { return trigger$.pipe( - exhaustMap((name: string) => { + exhaustMap(({ name, onDelete }) => { return this.testRunService.deleteProfile(name).pipe( withLatestFrom(this.profiles$), tap(([remove, current]) => { if (remove) { + const idx = current.findIndex(item => name === item.name); this.removeProfile(name, current); + onDelete(idx); } }) ); @@ -76,13 +92,15 @@ export class RiskAssessmentStore extends ComponentStore { return trigger$.pipe( withLatestFrom(this.profiles$), tap(([{ nextItem, firstItem }, profiles]) => { - if (nextItem) { - this.focusManagerService.focusFirstElementInContainer(nextItem); - } else if (profiles.length > 1) { - this.focusManagerService.focusFirstElementInContainer(firstItem); - } else { - this.focusManagerService.focusFirstElementInContainer(); - } + timer(100).subscribe(() => { + if (nextItem) { + this.focusManagerService.focusFirstElementInContainer(nextItem); + } else if (profiles.length > 1) { + this.focusManagerService.focusFirstElementInContainer(firstItem); + } else { + this.focusManagerService.focusFirstElementInContainer(); + } + }); }) ); } @@ -93,7 +111,7 @@ export class RiskAssessmentStore extends ComponentStore { delay(10), tap(() => { this.focusManagerService.focusFirstElementInContainer( - window.document.querySelector('.risk-assessment-content-empty') + window.document.querySelector('app-risk-assessment') ); }) ); @@ -103,12 +121,22 @@ export class RiskAssessmentStore extends ComponentStore { return trigger$.pipe( tap(() => { this.focusManagerService.focusFirstElementInContainer( - window.document.querySelector('.profiles-drawer-content .selected') + window.document.querySelector('.entity-list .selected') ); }) ); }); + scrollToSelectedProfile = this.effect(trigger$ => { + return trigger$.pipe( + tap(() => { + window.document + .querySelector('.entity-list .selected') + ?.scrollIntoView(); + }) + ); + }); + setFocusOnProfileForm = this.effect(trigger$ => { return trigger$.pipe( tap(() => { @@ -131,38 +159,54 @@ export class RiskAssessmentStore extends ComponentStore { ); }); - saveProfile = this.effect(trigger$ => { + saveProfile = this.effect<{ + profile: Profile; + onSave: ((profile: Profile) => void) | undefined; + }>(trigger$ => { return trigger$.pipe( - exhaustMap((name: Profile) => { - return this.testRunService.saveProfile(name).pipe( - tap(saved => { + exhaustMap(({ profile, onSave }) => { + return this.testRunService.saveProfile(profile).pipe( + exhaustMap(saved => { if (saved) { - this.store.dispatch(fetchRiskProfiles()); + return this.testRunService.fetchProfiles(); + } + return throwError('Failed to upload profile'); + }), + tap(newProfiles => { + this.store.dispatch(setRiskProfiles({ riskProfiles: newProfiles })); + const uploadedProfile = newProfiles.find( + p => p.name === profile.name || p.name === profile.rename + ); + if (onSave && uploadedProfile) { + onSave(uploadedProfile); } + }), + catchError(() => { + return EMPTY; }) ); }) ); }); - private removeProfile(name: string, current: Profile[]): void { - const profiles = current.filter(profile => profile.name !== name); - this.updateProfiles(profiles); + updateProfiles(riskProfiles: Profile[]): void { + this.store.dispatch(setRiskProfiles({ riskProfiles })); } - private updateProfiles(riskProfiles: Profile[]): void { - this.store.dispatch(setRiskProfiles({ riskProfiles })); + removeProfile(name: string, current: Profile[]): void { + const profiles = current.filter(profile => profile.name !== name); + this.updateProfiles(profiles); } - constructor( - private testRunService: TestRunService, - private store: Store, - private focusManagerService: FocusManagerService - ) { + constructor() { super({ profiles: [], profileFormat: [], selectedProfile: null, + actions: [ + { action: ProfileAction.Copy, icon: 'content_copy' }, + { action: ProfileAction.Delete, icon: 'delete' }, + ], }); } } diff --git a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.scss b/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.scss deleted file mode 100644 index 16093e336..000000000 --- a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.scss +++ /dev/null @@ -1,69 +0,0 @@ -@use '@angular/material' as mat; -@import '../../../../../theming/colors'; -@import '../../../../../theming/variables'; - -:host { - padding-top: 16px; -} - -.setting-form-label { - font-size: 18px; - color: $dark-grey; -} - -:host:has(.two-ports-message) .internet-label { - padding-top: 16px; -} - -.setting-label-description { - font-family: $font-secondary; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.2px; - margin: 8px 0; - color: $dark-grey; -} - -.setting-option-value { - padding: 14px 16px; -} - -.option-value { - margin: 0; - font-family: Roboto; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 20px; - letter-spacing: 0.2px; - - &.top { - color: $grey-800; - } - - &.bottom { - color: $grey-700; - } -} - -.setting-field { - width: 100%; - ::ng-deep .mat-mdc-form-field-infix { - min-height: 76px; - display: flex; - align-items: center; - } - - ::ng-deep .mat-mdc-floating-label { - font-family: Roboto; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 20px; - letter-spacing: 0.2px; - } - - ::ng-deep .mat-mdc-floating-label:not(.mdc-floating-label--float-above) { - top: 35px; - } -} diff --git a/modules/ui/src/app/pages/settings/settings.component.html b/modules/ui/src/app/pages/settings/settings.component.html index 36849b42e..68ad9d5b4 100644 --- a/modules/ui/src/app/pages/settings/settings.component.html +++ b/modules/ui/src/app/pages/settings/settings.component.html @@ -13,111 +13,17 @@ See the License for the specific language governing permissions and limitations under the License. --> -
-

System settings

- -
-
-
-
-
- - - Warning! Testrun requires two ports to operate correctly. - + +

Settings

+
- - - - -
-

- If a port is missing from this list, you can - - Refresh - - the System settings -

- - - - -
- - Both interfaces must have different values - - -
- - - Warning! No ports is detected. - - -
- + + + + + diff --git a/modules/ui/src/app/pages/settings/settings.component.scss b/modules/ui/src/app/pages/settings/settings.component.scss index 770d79a52..b5c1d7770 100644 --- a/modules/ui/src/app/pages/settings/settings.component.scss +++ b/modules/ui/src/app/pages/settings/settings.component.scss @@ -13,136 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@use '@angular/material' as mat; -@import '../../../theming/colors'; -@import '../../../theming/variables'; :host { - display: flex; - flex-direction: column; - height: 100%; - flex: 1 0 auto; -} - -.settings-drawer-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 12px 16px 24px; - - &-title { - margin: 0; - font-size: 22px; - font-style: normal; - font-weight: 400; - line-height: 28px; - color: $dark-grey; - } - - &-button { - min-width: 24px; - width: 24px; - height: 24px; - margin: 4px; - padding: 8px; - box-sizing: content-box; - line-height: normal !important; - - .close-button-icon { - width: 24px; - height: 24px; - margin: 0; - } - - ::ng-deep * { - line-height: inherit !important; - } - } -} - -.setting-drawer-content { - padding: 0 16px 8px 16px; - overflow: hidden; - flex: 1; - - form { - display: grid; - height: 100%; - } - - .setting-drawer-content-form-empty { - grid-template-rows: repeat(2, auto) 1fr; - } -} - -.setting-drawer-content-inputs { overflow: auto; - margin: 0 -16px; - padding: 0 16px; -} - -.error-message-container { - display: block; - margin-top: auto; - padding-bottom: 8px; } -.error-message-container + .setting-drawer-footer { - margin-top: 0; +.toolbar { + height: auto; + padding: 22px 0px 18px 32px; } - -.message { - margin: 0; - padding: 6px 0 12px 0; - color: $grey-800; - font-family: $font-secondary; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.2px; +.title { + padding: 24px 0 16px; } -.setting-drawer-footer { - padding: 0 8px; - margin-top: auto; +.tab-item { display: flex; - flex-shrink: 0; - justify-content: flex-end; - - .close-button, - .save-button { - padding: 0 24px; - font-size: 14px; - font-weight: 500; - line-height: 20px; - letter-spacing: 0.25px; - } - - .close-button { - margin-right: 10px; - &:enabled { - color: $primary; - } - } -} - -.settings-disabled-overlay { - position: absolute; - width: 100%; - left: 0; - right: 0; - top: 75px; - bottom: 45px; - background-color: rgba(255, 255, 255, 0.7); - z-index: 2; + padding: 0px 32px; + align-items: center; + gap: 32px; } +.tab-group { + ::ng-deep .mat-mdc-tab-labels { + gap: 16px; + padding: 0 12px; + } -.disabled { - .message-link { - cursor: default; - pointer-events: none; - - &:focus-visible { - outline: none; - } + ::ng-deep.mat-mdc-tab { + padding: 0px 8px; } } diff --git a/modules/ui/src/app/pages/settings/settings.component.spec.ts b/modules/ui/src/app/pages/settings/settings.component.spec.ts index d11a30d54..d93a3bb0c 100644 --- a/modules/ui/src/app/pages/settings/settings.component.spec.ts +++ b/modules/ui/src/app/pages/settings/settings.component.spec.ts @@ -1,3 +1,6 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsComponent } from './settings.component'; /** * Copyright 2023 Google LLC * @@ -13,367 +16,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { SettingsComponent } from './settings.component'; -import { of } from 'rxjs'; -import { MatRadioModule } from '@angular/material/radio'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIcon, MatIconModule } from '@angular/material/icon'; -import { MatIconTestingModule } from '@angular/material/icon/testing'; -import { Component, Input } from '@angular/core'; -import { LiveAnnouncer } from '@angular/cdk/a11y'; -import SpyObj = jasmine.SpyObj; -import { MatInputModule } from '@angular/material/input'; -import { MatSelectModule } from '@angular/material/select'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { provideMockStore } from '@ngrx/store/testing'; -import { LoaderService } from '../../services/loader.service'; -import { SettingsStore } from './settings.store'; -import { - MOCK_INTERFACES, - MOCK_INTERNET_OPTIONS, - MOCK_SYSTEM_CONFIG_WITH_DATA, -} from '../../mocks/settings.mock'; -import { SettingsDropdownComponent } from './components/settings-dropdown/settings-dropdown.component'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; -describe('GeneralSettingsComponent', () => { +describe('SettingsComponent', () => { let component: SettingsComponent; let fixture: ComponentFixture; - let mockLiveAnnouncer: SpyObj; - let compiled: HTMLElement; - let mockLoaderService: SpyObj; - let mockSettingsStore: SpyObj; beforeEach(async () => { - mockLiveAnnouncer = jasmine.createSpyObj(['announce']); - mockLoaderService = jasmine.createSpyObj('LoaderService', ['setLoading']); - mockSettingsStore = jasmine.createSpyObj('SettingsStore', [ - 'getInterfaces', - 'updateSystemConfig', - 'setIsSubmitting', - 'setDefaultFormValues', - 'getSystemConfig', - 'viewModel$', - ]); - await TestBed.configureTestingModule({ - declarations: [ - SettingsComponent, - FakeSpinnerComponent, - FakeCalloutComponent, - ], - providers: [ - { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, - { provide: LoaderService, useValue: mockLoaderService }, - { provide: SettingsStore, useValue: mockSettingsStore }, - provideMockStore(), - ], - imports: [ - BrowserAnimationsModule, - MatButtonModule, - MatIconModule, - MatRadioModule, - ReactiveFormsModule, - MatIconTestingModule, - MatIcon, - MatInputModule, - MatSelectModule, - SettingsDropdownComponent, - ], + imports: [SettingsComponent, NoopAnimationsModule, RouterTestingModule], }).compileComponents(); - TestBed.overrideProvider(SettingsStore, { useValue: mockSettingsStore }); - fixture = TestBed.createComponent(SettingsComponent); - component = fixture.componentInstance; - component.viewModel$ = of({ - systemConfig: { network: {} }, - hasConnectionSettings: false, - isSubmitting: false, - isLessThanOneInterface: false, - interfaces: {}, - deviceOptions: {}, - internetOptions: {}, - logLevelOptions: {}, - monitoringPeriodOptions: {}, - }); fixture.detectChanges(); - compiled = fixture.nativeElement as HTMLElement; - - component.ngOnInit(); }); it('should create', () => { expect(component).toBeTruthy(); }); - - it('#reloadSetting should call setLoading in loaderService', () => { - component.reloadSetting(); - - expect(mockLoaderService.setLoading).toHaveBeenCalledWith(true); - }); - - describe('#settingsDisable', () => { - it('should disable setting form when get settingDisable as true ', () => { - spyOn(component.settingForm, 'disable'); - - component.settingsDisable = true; - - expect(component.settingForm.disable).toHaveBeenCalled(); - }); - - it('should enable setting form when get settingDisable as false ', () => { - spyOn(component.settingForm, 'enable'); - - component.settingsDisable = false; - - expect(component.settingForm.enable).toHaveBeenCalled(); - }); - - it('should disable "Save" button when get settingDisable as true', () => { - component.settingsDisable = true; - - const saveBtn = compiled.querySelector( - '.save-button' - ) as HTMLButtonElement; - - expect(saveBtn.disabled).toBeTrue(); - }); - - it('should disable "Refresh" link when settingDisable', () => { - component.settingsDisable = true; - - const refreshLink = compiled.querySelector( - '.message-link' - ) as HTMLAnchorElement; - - refreshLink.click(); - - expect(refreshLink.hasAttribute('aria-disabled')).toBeTrue(); - expect(mockLoaderService.setLoading).not.toHaveBeenCalled(); - }); - }); - - describe('#closeSetting', () => { - beforeEach(() => { - component.ngOnInit(); - }); - - it('should emit closeSettingEvent', () => { - spyOn(component.closeSettingEvent, 'emit'); - - component.closeSetting('Message'); - - expect(component.closeSettingEvent.emit).toHaveBeenCalled(); - }); - - it('should call liveAnnouncer with provided message', () => { - const mockMessage = 'mock event'; - - component.closeSetting(mockMessage); - - expect(mockLiveAnnouncer.announce).toHaveBeenCalledWith( - `The ${mockMessage} finished. The system settings panel is closed.` - ); - }); - - it('should call reset settingForm', () => { - spyOn(component.settingForm, 'reset'); - - component.closeSetting('Message'); - - expect(component.settingForm.reset).toHaveBeenCalled(); - }); - - it('should call setDefaultFormValues', () => { - component.closeSetting('Message'); - - expect(mockSettingsStore.setDefaultFormValues).toHaveBeenCalled(); - }); - }); - - describe('#saveSetting', () => { - beforeEach(() => { - component.ngOnInit(); - }); - - it('should have form error if form has the same value', () => { - const mockSameValue = 'sameValue'; - component.deviceControl.setValue(mockSameValue); - component.internetControl.setValue(mockSameValue); - - component.saveSetting(); - - expect(component.settingForm.invalid).toBeTrue(); - expect(component.isFormError).toBeTrue(); - expect(mockSettingsStore.setIsSubmitting).toHaveBeenCalledWith(true); - }); - - it('should call createSystemConfig when setting form valid', () => { - const expectedResult = { - network: { - device_intf: 'mockDeviceKey', - internet_intf: '', - }, - log_level: 'INFO', - monitor_period: 600, - }; - - component.deviceControl.setValue({ - key: 'mockDeviceKey', - value: 'mockDeviceValue', - }); - - component.internetControl.setValue({ - key: '', - value: 'defaultValue', - }); - - component.logLevel.setValue({ - key: 'INFO', - value: '', - }); - - component.monitorPeriod.setValue({ - key: '600', - value: '', - }); - - component.saveSetting(); - - const args = mockSettingsStore.updateSystemConfig.calls.argsFor(0); - // @ts-expect-error config is in object - expect(args[0].config).toEqual(expectedResult); - expect(component.settingForm.invalid).toBeFalse(); - expect(mockSettingsStore.updateSystemConfig).toHaveBeenCalled(); - }); - }); - - describe('with no interfaces data', () => { - beforeEach(() => { - component.viewModel$ = of({ - systemConfig: { network: {} }, - hasConnectionSettings: false, - isSubmitting: false, - isLessThanOneInterface: false, - interfaces: {}, - deviceOptions: {}, - internetOptions: {}, - logLevelOptions: {}, - monitoringPeriodOptions: {}, - }); - fixture.detectChanges(); - }); - - it('should have callout component', () => { - const callout = compiled.querySelector('app-callout'); - - expect(callout).toBeTruthy(); - }); - - it('should have disabled "Save" button', () => { - const saveBtn = compiled.querySelector( - '.save-button' - ) as HTMLButtonElement; - - expect(saveBtn.disabled).toBeTrue(); - }); - }); - - describe('with interfaces length less than one', () => { - beforeEach(() => { - component.viewModel$ = of({ - systemConfig: { network: {} }, - hasConnectionSettings: false, - isSubmitting: false, - isLessThanOneInterface: true, - interfaces: {}, - deviceOptions: {}, - internetOptions: {}, - logLevelOptions: {}, - monitoringPeriodOptions: {}, - }); - fixture.detectChanges(); - }); - - it('should have callout component', () => { - const callout = compiled.querySelector('app-callout'); - - expect(callout).toBeTruthy(); - }); - - it('should have disabled "Save" button', () => { - component.deviceControl.setValue( - MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.device_intf - ); - component.internetControl.setValue( - MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.internet_intf - ); - fixture.detectChanges(); - - const saveBtn = compiled.querySelector( - '.save-button' - ) as HTMLButtonElement; - - expect(saveBtn.disabled).toBeTrue(); - }); - }); - - describe('with interfaces length more then one', () => { - beforeEach(() => { - component.viewModel$ = of({ - systemConfig: { network: {} }, - hasConnectionSettings: false, - isSubmitting: false, - isLessThanOneInterface: false, - interfaces: MOCK_INTERFACES, - deviceOptions: MOCK_INTERFACES, - internetOptions: MOCK_INTERNET_OPTIONS, - logLevelOptions: {}, - monitoringPeriodOptions: {}, - }); - fixture.detectChanges(); - }); - - it('should not have callout component', () => { - const callout = compiled.querySelector('app-callout'); - - expect(callout).toBeFalsy(); - }); - - it('should not have disabled "Save" button', () => { - component.deviceControl.setValue({ - key: MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.device_intf, - value: 'value', - }); - component.internetControl.setValue({ - key: MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.internet_intf, - value: 'value', - }); - fixture.detectChanges(); - - const saveBtn = compiled.querySelector( - '.save-button' - ) as HTMLButtonElement; - - expect(saveBtn.disabled).toBeFalse(); - }); - }); }); - -@Component({ - selector: 'app-spinner', - template: '
', -}) -class FakeSpinnerComponent {} - -@Component({ - selector: 'app-callout', - template: '
', -}) -class FakeCalloutComponent { - @Input() type = ''; -} diff --git a/modules/ui/src/app/pages/settings/settings.component.ts b/modules/ui/src/app/pages/settings/settings.component.ts index 1f9b4f62a..6f3bb376c 100644 --- a/modules/ui/src/app/pages/settings/settings.component.ts +++ b/modules/ui/src/app/pages/settings/settings.component.ts @@ -16,199 +16,66 @@ import { ChangeDetectionStrategy, Component, - ElementRef, - EventEmitter, - Input, OnDestroy, OnInit, - Output, - ViewChild, } from '@angular/core'; -import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; -import { Subject, takeUntil, tap } from 'rxjs'; -import { OnlyDifferentValuesValidator } from './only-different-values.validator'; -import { CalloutType } from '../../model/callout-type'; -import { CdkTrapFocus, LiveAnnouncer } from '@angular/cdk/a11y'; -import { EventType } from '../../model/event-type'; -import { FormKey, SystemConfig } from '../../model/setting'; -import { SettingsStore } from './settings.store'; -import { LoaderService } from '../../services/loader.service'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { CommonModule } from '@angular/common'; +import { MatTabChangeEvent, MatTabsModule } from '@angular/material/tabs'; +import { + ActivatedRoute, + NavigationEnd, + Router, + RouterModule, +} from '@angular/router'; +import { Routes } from '../../model/routes'; +import { filter, Subject, takeUntil } from 'rxjs'; @Component({ selector: 'app-settings', + imports: [CommonModule, MatToolbarModule, MatTabsModule, RouterModule], templateUrl: './settings.component.html', styleUrls: ['./settings.component.scss'], - hostDirectives: [CdkTrapFocus], - providers: [SettingsStore], changeDetection: ChangeDetectionStrategy.OnPush, }) export class SettingsComponent implements OnInit, OnDestroy { - @ViewChild('reloadSettingLink') public reloadSettingLink!: ElementRef; - @Output() closeSettingEvent = new EventEmitter(); - - private isSettingsDisable = false; - get settingsDisable(): boolean { - return this.isSettingsDisable; - } - @Input() set settingsDisable(value: boolean) { - this.isSettingsDisable = value; - value ? this.disableSettings() : this.enableSettings(); - } - public readonly CalloutType = CalloutType; - public readonly EventType = EventType; - public readonly FormKey = FormKey; - public settingForm!: FormGroup; - viewModel$ = this.settingsStore.viewModel$; - + private routes = [Routes.General, Routes.Certificates]; private destroy$: Subject = new Subject(); - - get deviceControl(): FormControl { - return this.settingForm.get(FormKey.DEVICE) as FormControl; - } - - get internetControl(): FormControl { - return this.settingForm.get(FormKey.INTERNET) as FormControl; - } - - get logLevel(): FormControl { - return this.settingForm.get(FormKey.LOG_LEVEL) as FormControl; - } - - get monitorPeriod(): FormControl { - return this.settingForm.get(FormKey.MONITOR_PERIOD) as FormControl; - } - - get isFormValues(): boolean { - return this.internetControl?.value.value && this.deviceControl?.value.value; - } - - get isFormError(): boolean { - return this.settingForm.hasError('hasSameValues'); - } - + selectedIndex = 0; constructor( - private readonly fb: FormBuilder, - private liveAnnouncer: LiveAnnouncer, - private readonly onlyDifferentValuesValidator: OnlyDifferentValuesValidator, - private settingsStore: SettingsStore, - private readonly loaderService: LoaderService + private router: Router, + private route: ActivatedRoute ) {} - ngOnInit() { - this.createSettingForm(); - this.cleanFormErrorMessage(); - this.settingsStore.getInterfaces(); - this.settingsStore.getSystemConfig(); - this.setDefaultFormValues(); - } + ngOnInit(): void { + const currentRoute = this.router.url; + this.setSelectedIndex(currentRoute); - reloadSetting(): void { - if (this.settingsDisable) { - return; - } - this.showLoading(); - this.getSystemInterfaces(); - this.settingsStore.getSystemConfig(); - this.setDefaultFormValues(); - } - closeSetting(message: string): void { - this.resetForm(); - this.closeSettingEvent.emit(); - this.liveAnnouncer.announce( - `The ${message} finished. The system settings panel is closed.` - ); - this.setDefaultFormValues(); - } - - saveSetting(): void { - if (this.settingForm.invalid) { - this.settingsStore.setIsSubmitting(true); - this.settingForm.markAllAsTouched(); - } else { - this.createSystemConfig(); - } - } - - private disableSettings(): void { - this.settingForm?.disable(); - this.reloadSettingLink?.nativeElement.setAttribute('aria-disabled', 'true'); - } - - private enableSettings(): void { - this.settingForm?.enable(); - this.reloadSettingLink?.nativeElement.removeAttribute('aria-disabled'); - } - - private createSettingForm() { - this.settingForm = this.fb.group( - { - device_intf: [''], - internet_intf: [''], - log_level: [''], - monitor_period: [''], - }, - { - validators: [this.onlyDifferentValuesValidator.onlyDifferentSetting()], - updateOn: 'change', - } - ); - } - - private setDefaultFormValues() { - this.settingsStore.setDefaultFormValues(this.settingForm); - } - - private cleanFormErrorMessage(): void { - this.settingForm.valueChanges + this.router.events .pipe( takeUntil(this.destroy$), - tap(() => this.settingsStore.setIsSubmitting(false)) + filter(event => event instanceof NavigationEnd) ) - .subscribe(); + .subscribe(event => { + if (event.url !== currentRoute) { + this.setSelectedIndex(event.url); + } + }); } - private createSystemConfig(): void { - const { device_intf, internet_intf, log_level, monitor_period } = - this.settingForm.value; - const data: SystemConfig = { - network: { - device_intf: device_intf.key, - internet_intf: internet_intf.key, - }, - log_level: log_level.key, - monitor_period: Number(monitor_period.key), - }; - this.settingsStore.updateSystemConfig({ - onSystemConfigUpdate: () => { - this.closeSetting(EventType.Save); - }, - config: data, - }); + onTabChange(event: MatTabChangeEvent): void { + const index = event.index; + this.router.navigate([this.routes[index]], { relativeTo: this.route }); } - private resetForm(): void { - this.settingForm.reset(); + private setSelectedIndex(currentRoute: string): void { + this.selectedIndex = this.routes.findIndex(route => + currentRoute.includes(route) + ); } ngOnDestroy() { this.destroy$.next(true); this.destroy$.unsubscribe(); } - - getSystemInterfaces(): void { - this.settingsStore.getInterfaces(); - this.hideLoading(); - } - - getSystemConfig(): void { - this.settingsStore.getSystemConfig(); - } - - private showLoading() { - this.loaderService.setLoading(true); - } - - private hideLoading() { - this.loaderService.setLoading(false); - } } diff --git a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.html b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.html index 6dc3b10ac..8c41759bd 100644 --- a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.html +++ b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.html @@ -13,37 +13,53 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - lab_profile - - {{ DownloadOption.PDF }} - - - - - archive - - {{ DownloadOption.ZIP }} - - - - +
+ + + + archive + + {{ DownloadOption.ZIP }} + + +
diff --git a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.scss b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.scss index cf368929d..5e8f7da54 100644 --- a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.scss +++ b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.scss @@ -15,80 +15,77 @@ */ @use 'node_modules/@angular/material/index' as mat; -@import 'src/theming/colors'; +@use 'm3-theme' as *; +@use 'colors'; +@use 'variables'; -$option-width: 170px; -$option-height: 36px; - -.download-options-field { - width: $option-width; - background: mat.get-color-from-palette($color-primary, 50); - - ::ng-deep.mat-mdc-text-field-wrapper { - padding: 0 16px; - } - - ::ng-deep.mat-mdc-form-field-subscript-wrapper { - display: none; - } - - ::ng-deep.mat-mdc-form-field-infix { - padding: 6px 0; - width: $option-width; - min-height: $option-height; - } - - ::ng-deep.mat-mdc-select-placeholder { - color: mat.get-color-from-palette($color-primary, 700); - font-size: 14px; - font-style: normal; - font-weight: 500; - line-height: 20px; - letter-spacing: 0.25px; - } +.download-actions { + position: fixed; + right: 40px; + bottom: 32px; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: flex-end; + gap: 4px; +} - ::ng-deep.mat-mdc-select-arrow { - color: mat.get-color-from-palette($color-primary, 700); - } +.download-button { + padding: 26px; + border-radius: variables.$corner-large; + font-weight: 400; + font-size: 22px; + line-height: 28px; + height: 80px; + margin-top: 4px; - ::ng-deep .mat-mdc-text-field-wrapper .mdc-notched-outline > * { - border-color: mat.get-color-from-palette($color-primary, 50); - } - - ::ng-deep - .mat-mdc-text-field-wrapper.mdc-text-field--focused - .mdc-notched-outline - > * { - border-color: #000000de; + mat-icon { + font-size: 28px; + width: 28px; + height: 28px; } +} - &:has(.download-options-select[aria-expanded='true']) - ::ng-deep - .mat-mdc-text-field-wrapper.mdc-text-field--focused - .mdc-notched-outline - > * { - border: none; - } +.download-button.download-button-opened { + min-width: 56px; + height: 56px; + border-radius: 50%; + padding: 16px; - ::ng-deep - .mat-mdc-text-field-wrapper.mdc-text-field--outlined:hover - .mdc-notched-outline - > * { - border: none; + mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + margin: 0; } } .download-option { - ::ng-deep .mat-mdc-focus-indicator { - display: none; + display: flex; + height: 56px; + box-sizing: border-box; + padding: 16px 24px; + justify-content: flex-end; + align-items: center; + flex-shrink: 0; + gap: 8px; + border-radius: 28px; + background: colors.$primary-container; + color: colors.$on-primary-container; + font-size: 16px; + font-weight: 500; + line-height: 24px; + + mat-icon { + width: 24px; + height: 24px; + font-size: 24px; + margin: 0; } } -.download-option { - min-height: 32px; - color: $grey-800; - &.zip app-download-report-zip { - display: flex; - align-items: center; +button { + ::ng-deep .mat-focus-indicator { + display: none; } } diff --git a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.spec.ts b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.spec.ts index 2b370485f..a9be3de24 100644 --- a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.spec.ts +++ b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.spec.ts @@ -15,17 +15,13 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { - DownloadOption, - DownloadOptionsComponent, -} from './download-options.component'; +import { DownloadOptionsComponent } from './download-options.component'; import { MOCK_PROGRESS_DATA_CANCELLED, MOCK_PROGRESS_DATA_COMPLIANT, MOCK_PROGRESS_DATA_NON_COMPLIANT, } from '../../../../mocks/testrun.mock'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MatOptionSelectionChange } from '@angular/material/core'; import { TestRunService } from '../../../../services/test-run.service'; interface GAEvent { @@ -63,43 +59,33 @@ describe('DownloadOptionsComponent', () => { expect(downloadReportZipComponent).toBeDefined(); }); - it('#onSelected should call getReportTitle', () => { + it('#downloadPdf should call getReportTitle', () => { const spyGetReportTitle = spyOn(component, 'getReportTitle'); - const mockEvent = { - source: {}, - isUserInput: true, - } as MatOptionSelectionChange; - - component.onSelected( - mockEvent, - MOCK_PROGRESS_DATA_COMPLIANT, - DownloadOption.PDF - ); + component.downloadPdf(MOCK_PROGRESS_DATA_COMPLIANT); expect(spyGetReportTitle).toHaveBeenCalled(); }); - it('#onSelected should call getZipLink when using for zip report', () => { - const spyGetZipLink = spyOn(component, 'getZipLink'); + it('#getReportTitle should return data for title of link', () => { + const expectedResult = 'delta_03-din-cpu_1.2.2_complete_22_jun_2023_9:20'; - const mockEvent = { - source: {}, - isUserInput: true, - } as MatOptionSelectionChange; + const result = component.getReportTitle(MOCK_PROGRESS_DATA_COMPLIANT); - component.onSelected( - mockEvent, - MOCK_PROGRESS_DATA_COMPLIANT, - DownloadOption.ZIP - ); + expect(result).toEqual(expectedResult); + }); + + it('#openDownloadOptions should change isOpenDownloadOptions', () => { + component.openDownloadOptions(); + expect(component.isOpenDownloadOptions).toBeTrue(); - expect(spyGetZipLink).toHaveBeenCalled(); + component.openDownloadOptions(); + expect(component.isOpenDownloadOptions).toBeFalse(); }); describe('#sendGAEvent', () => { it('should send download_report_pdf when type is pdf', () => { - component.sendGAEvent(MOCK_PROGRESS_DATA_CANCELLED, DownloadOption.PDF); + component.sendGAEvent(MOCK_PROGRESS_DATA_CANCELLED); expect( // @ts-expect-error data layer should be defined @@ -109,8 +95,8 @@ describe('DownloadOptionsComponent', () => { ).toBeTruthy(); }); - it('should send download_report_pdf_compliant when type is pdf and status is compliant', () => { - component.sendGAEvent(MOCK_PROGRESS_DATA_COMPLIANT, DownloadOption.PDF); + it('should send download_report_pdf_compliant when status is compliant', () => { + component.sendGAEvent(MOCK_PROGRESS_DATA_COMPLIANT); expect( // @ts-expect-error data layer should be defined @@ -120,11 +106,8 @@ describe('DownloadOptionsComponent', () => { ).toBeTruthy(); }); - it('should send download_report_pdf_non_compliant when type is pdf and status is not compliant', () => { - component.sendGAEvent( - MOCK_PROGRESS_DATA_NON_COMPLIANT, - DownloadOption.PDF - ); + it('should send download_report_pdf_non_compliant when status is not compliant', () => { + component.sendGAEvent(MOCK_PROGRESS_DATA_NON_COMPLIANT); expect( // @ts-expect-error data layer should be defined @@ -133,41 +116,5 @@ describe('DownloadOptionsComponent', () => { ) ).toBeTruthy(); }); - - it('should send download_report_zip when type is zip', () => { - component.sendGAEvent(MOCK_PROGRESS_DATA_CANCELLED, DownloadOption.ZIP); - - expect( - // @ts-expect-error data layer should be defined - window.dataLayer.some( - (item: GAEvent) => item.event === 'download_report_zip' - ) - ).toBeTruthy(); - }); - - it('should send download_report_zip_compliant when type is pdf and status is compliant', () => { - component.sendGAEvent(MOCK_PROGRESS_DATA_COMPLIANT, DownloadOption.ZIP); - - expect( - // @ts-expect-error data layer should be defined - window.dataLayer.some( - (item: GAEvent) => item.event === 'download_report_zip_compliant' - ) - ).toBeTruthy(); - }); - - it('should send download_report_zip_non_compliant when type is zip and status is not compliant', () => { - component.sendGAEvent( - MOCK_PROGRESS_DATA_NON_COMPLIANT, - DownloadOption.ZIP - ); - - expect( - // @ts-expect-error data layer should be defined - window.dataLayer.some( - (item: GAEvent) => item.event === 'download_report_zip_non_compliant' - ) - ).toBeTruthy(); - }); }); }); diff --git a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.ts b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.ts index fddd6822f..87e13e098 100644 --- a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.ts +++ b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.ts @@ -16,22 +16,18 @@ import { ChangeDetectionStrategy, Component, - ElementRef, Input, - ViewChild, + inject, } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatSelectModule } from '@angular/material/select'; import { CommonModule, DatePipe } from '@angular/common'; import { MatIconModule } from '@angular/material/icon'; import { - StatusOfTestrun, + ResultOfTestrun, TestrunStatus, } from '../../../../model/testrun-status'; -import { MatOptionSelectionChange } from '@angular/material/core'; import { DownloadReportZipComponent } from '../../../../components/download-report-zip/download-report-zip.component'; import { Profile } from '../../../../model/profile'; +import { MatButtonModule } from '@angular/material/button'; export enum DownloadOption { PDF = 'PDF Report', @@ -41,62 +37,42 @@ export enum DownloadOption { selector: 'app-download-options', templateUrl: './download-options.component.html', styleUrl: './download-options.component.scss', - standalone: true, imports: [ CommonModule, - FormsModule, + MatButtonModule, MatIconModule, - MatFormFieldModule, - MatSelectModule, DownloadReportZipComponent, ], providers: [DatePipe], changeDetection: ChangeDetectionStrategy.OnPush, }) export class DownloadOptionsComponent { - @ViewChild('downloadReportZip') downloadReportZip!: ElementRef; + private datePipe = inject(DatePipe); + isOpenDownloadOptions: boolean = false; @Input() profiles: Profile[] = []; @Input() data!: TestrunStatus; DownloadOption = DownloadOption; - constructor(private datePipe: DatePipe) {} - onSelected( - event: MatOptionSelectionChange, - data: TestrunStatus, - type: string - ) { - if (event.isUserInput) { - this.createLink(data, type); - this.sendGAEvent(data, type); - } + downloadPdf(data: TestrunStatus) { + this.createLink(data); + this.sendGAEvent(data); } - onZipSelected(event: MatOptionSelectionChange) { - if (event.isUserInput) { - const uploadCertificatesButton = document.querySelector( - '#downloadReportZip' - ) as HTMLElement; - uploadCertificatesButton.dispatchEvent(new MouseEvent('click')); - } - } - - createLink(data: TestrunStatus, type: string) { + createLink(data: TestrunStatus) { if (!data.report) { return; } const link = document.createElement('a'); - link.href = - type === DownloadOption.PDF ? data.report : this.getZipLink(data.report); + link.href = data.report; link.target = '_blank'; link.download = this.getReportTitle(data); link.dispatchEvent(new MouseEvent('click')); } - getZipLink(reportURL: string): string { - return reportURL.replace('report', 'export'); - } - getReportTitle(data: TestrunStatus) { + if (!data.device) { + return ''; + } return `${data.device.manufacturer} ${data.device.model} ${ data.device.firmware } ${data.status} ${this.getFormattedDateString(data.started)}` @@ -108,11 +84,11 @@ export class DownloadOptionsComponent { return date ? this.datePipe.transform(date, 'd MMM y H:mm') : ''; } - sendGAEvent(data: TestrunStatus, type: string) { - let event = `download_report_${type === DownloadOption.PDF ? 'pdf' : 'zip'}`; - if (data.status === StatusOfTestrun.Compliant) { + sendGAEvent(data: TestrunStatus) { + let event = 'download_report_pdf'; + if (data.result === ResultOfTestrun.Compliant) { event += '_compliant'; - } else if (data.status === StatusOfTestrun.NonCompliant) { + } else if (data.result === ResultOfTestrun.NonCompliant) { event += '_non_compliant'; } // @ts-expect-error data layer is not null @@ -120,4 +96,8 @@ export class DownloadOptionsComponent { event: event, }); } + + openDownloadOptions(): void { + this.isOpenDownloadOptions = !this.isOpenDownloadOptions; + } } diff --git a/modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.html b/modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.html new file mode 100644 index 000000000..e8bacf230 --- /dev/null +++ b/modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.html @@ -0,0 +1,46 @@ + +{{ data.testResult.name }} + +
+
+

Description

+

{{ data.testResult.description }}

+
+
+

Test result

+

{{ data.testResult.result }}

+

{{ data.testResult.required_result }}

+
+
+
+

Steps to resolve

+
    +
  • {{ point }}
  • +
+
+
+ + + diff --git a/modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.scss b/modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.scss new file mode 100644 index 000000000..3fbe2ffca --- /dev/null +++ b/modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.scss @@ -0,0 +1,109 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use 'colors'; +@use 'variables'; +@use 'mixins'; + +::ng-deep :root { + --mat-dialog-container-max-width: 640px; +} + +:host { + @include mixins.dialog; +} + +.dialog-title { + background: colors.$white; + padding: 48px 24px 40px; + text-align: center; + font-size: 24px; + font-weight: 500; + line-height: 32px; + letter-spacing: 0; + &:before { + height: 0; + } +} + +p { + margin: 0; +} + +mat-dialog-content.content-container { + padding: 0; +} + +.result-info-main { + display: grid; + background: colors.$error-container; + grid-template-columns: 1.2fr 1fr; + padding: 32px 48px; + gap: 48px; +} + +.info-label { + color: colors.$on-surface-variant; + font-size: 12px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.1px; + + &.steps { + color: colors.$on-surface; + } +} + +.info-main-data, +.info-required-result { + color: colors.$on-surface; + font-size: 16px; + line-height: 24px; + letter-spacing: 0; +} + +.info-result { + color: colors.$on-error-container; + font-family: variables.$font-primary; + font-size: 28px; + line-height: 36px; + letter-spacing: 0; +} + +.result-info-steps { + padding: 32px 48px; +} + +.info-steps-list { + margin: 0; + padding-left: 24px; + font-size: 16px; + line-height: 24px; + letter-spacing: 0; +} + +mat-dialog-actions.actions-container { + padding: 18px 36px 26px; +} + +.close-button { + border-radius: variables.$corner-medium; + padding: 0 16px; + min-width: 54px; + + ::ng-deep .mat-focus-indicator { + display: none; + } +} diff --git a/modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.spec.ts b/modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.spec.ts new file mode 100644 index 000000000..057ed1e4e --- /dev/null +++ b/modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.spec.ts @@ -0,0 +1,68 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TestResultDialogComponent } from './test-result-dialog.component'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { TEST_DATA_RESULT_WITH_RECOMMENDATIONS } from '../../../../mocks/testrun.mock'; + +describe('TestResultDialogComponent', () => { + let component: TestResultDialogComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestResultDialogComponent], + providers: [ + { + provide: MatDialogRef, + useValue: { + keydownEvents: () => of(new KeyboardEvent('keydown', { code: '' })), + close: () => ({}), + }, + }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestResultDialogComponent); + component = fixture.componentInstance; + component.data = { + testResult: TEST_DATA_RESULT_WITH_RECOMMENDATIONS[0], + }; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should close dialog on click "Close" button', () => { + const closeSpy = spyOn(component.dialogRef, 'close'); + const closeButton = compiled.querySelector( + '.close-button' + ) as HTMLButtonElement; + + closeButton?.click(); + + expect(closeSpy).toHaveBeenCalled(); + + closeSpy.calls.reset(); + }); +}); diff --git a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.ts b/modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.ts similarity index 51% rename from modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.ts rename to modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.ts index af42aabe9..8a05ea11c 100644 --- a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.ts +++ b/modules/ui/src/app/pages/testrun/components/test-result-dialog/test-result-dialog.component.ts @@ -13,41 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; -import { EscapableDialogComponent } from '../escapable-dialog/escapable-dialog.component'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef, } from '@angular/material/dialog'; import { MatButtonModule } from '@angular/material/button'; +import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; +import { IResult } from '../../../../model/testrun-status'; interface DialogData { - title?: string; - content?: string; + testResult: IResult; } @Component({ - selector: 'app-shutdown-app-modal', - templateUrl: './shutdown-app-modal.component.html', - styleUrl: './shutdown-app-modal.component.scss', - standalone: true, - imports: [MatDialogModule, MatButtonModule], + selector: 'app-test-result-dialog', + imports: [MatDialogModule, MatButtonModule, CommonModule], + templateUrl: './test-result-dialog.component.html', + styleUrl: './test-result-dialog.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ShutdownAppModalComponent extends EscapableDialogComponent { - constructor( - public override dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: DialogData - ) { - super(dialogRef); - } +export class TestResultDialogComponent extends EscapableDialogComponent { + override dialogRef: MatDialogRef; + data = inject(MAT_DIALOG_DATA); - confirm() { - this.dialogRef.close(true); - } + constructor() { + const dialogRef = + inject>(MatDialogRef); - cancel() { - this.dialogRef.close(); + super(); + this.dialogRef = dialogRef; } } diff --git a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.html b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.html index c576d242e..6347ec162 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.html +++ b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.html @@ -14,7 +14,7 @@ limitations under the License. -->
- Start New Testrun + Start new Testrun
- + {{ error$ | async }} + + + Top Tip: Your device must be powered off before starting Testrun + +
@@ -88,7 +95,7 @@ color="primary" mat-flat-button type="button"> - Start Testrun + Start new Testrun diff --git a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.scss b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.scss index 082599567..20c4a0048 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.scss +++ b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.scss @@ -13,14 +13,47 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import 'src/theming/colors'; -@import 'src/theming/variables'; +@use 'colors'; +@use 'variables'; +@use 'mixins'; :host { display: grid; grid-template-rows: 1fr; overflow: hidden; - width: 450px; + width: 490px; + background: colors.$surface-container; + + app-device-tests { + padding-left: 16px; + + ::ng-deep .device-form-test-modules { + min-height: 78px; + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(3, 1fr); + grid-auto-flow: column; + padding-top: 8px; + padding-left: 24px; + + p { + margin: 6px 0; + } + } + + ::ng-deep .device-tests-title { + margin: 16px 0 0; + font-size: 22px; + line-height: 28px; + } + } + + app-callout { + ::ng-deep .callout-container.info { + margin: 8px 0 0; + padding: 16px 16px 12px; + } + } } .progress-initiate-form { @@ -30,25 +63,23 @@ } .progress-initiate-form-title { - color: $grey-800; - font-size: 22px; - line-height: 28px; - padding: 24px; - border-bottom: 1px solid $light-grey; + @include mixins.headline-large; + padding: 24px 24px 20px; + text-align: center; } .progress-initiate-form-content { overflow: auto; min-height: 78px; - padding: 32px 0; + padding: 4px 24px 8px; display: grid; - gap: 24px; + gap: 8px; justify-content: center; justify-items: center; grid-template-columns: 1fr; & > * { - width: $device-item-width; + width: variables.$device-item-width; box-sizing: border-box; } } @@ -56,8 +87,31 @@ .progress-initiate-form-actions { min-height: 30px; justify-content: space-between; - padding: 16px; - border-top: 1px solid $lighter-grey; + padding: 24px 32px; + + button { + border-radius: variables.$corner-medium; + } + + .progress-initiate-form-actions-change-device { + margin-right: auto; + } +} + +.progress-initiate-form-actions-change-device[disabled] + ::ng-deep + .mat-mdc-button-persistent-ripple::before { + opacity: 1; + background: rgba(31, 31, 31, 0.1); + color: colors.$on-surface; +} + +.selected-device { + margin-bottom: 16px; +} + +.device-tests-error { + padding-left: 16px; } .hidden { diff --git a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.spec.ts b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.spec.ts index dd614c9d1..f2157b2bd 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.spec.ts +++ b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.spec.ts @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; import { TestrunInitiateFormComponent } from './testrun-initiate-form.component'; import { @@ -23,13 +28,13 @@ import { } from '@angular/material/dialog'; import { TestRunService } from '../../../../services/test-run.service'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { Device } from '../../../../model/device'; +import { Device, DeviceStatus } from '../../../../model/device'; import { DeviceItemComponent } from '../../../../components/device-item/device-item.component'; import { ReactiveFormsModule } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { DeviceTestsComponent } from '../../../../components/device-tests/device-tests.component'; -import { device } from '../../../../mocks/device.mock'; +import { device, MOCK_TEST_MODULES } from '../../../../mocks/device.mock'; import { of } from 'rxjs'; import { MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE } from '../../../../mocks/testrun.mock'; import { SpinnerComponent } from '../../../../components/spinner/spinner.component'; @@ -44,25 +49,12 @@ describe('ProgressInitiateFormComponent', () => { const testRunServiceMock = jasmine.createSpyObj([ 'getDevices', 'fetchDevices', - 'getTestModules', 'startTestrun', 'systemStatus$', 'getSystemStatus', 'fetchVersion', 'setIsOpenStartTestrun', ]); - testRunServiceMock.getTestModules.and.returnValue([ - { - displayName: 'Connection', - name: 'connection', - enabled: true, - }, - { - displayName: 'DNS', - name: 'dns', - enabled: false, - }, - ]); testRunServiceMock.getDevices.and.returnValue( new BehaviorSubject([device, device]) ); @@ -71,7 +63,6 @@ describe('ProgressInitiateFormComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [TestrunInitiateFormComponent], providers: [ { provide: TestRunService, useValue: testRunServiceMock }, { @@ -81,12 +72,16 @@ describe('ProgressInitiateFormComponent', () => { close: () => ({}), }, }, - { provide: MAT_DIALOG_DATA, useValue: {} }, + { + provide: MAT_DIALOG_DATA, + useValue: { testModules: MOCK_TEST_MODULES }, + }, provideMockStore({ selectors: [{ selector: selectDevices, value: [device, device] }], }), ], imports: [ + TestrunInitiateFormComponent, MatDialogModule, DeviceItemComponent, ReactiveFormsModule, @@ -206,6 +201,7 @@ describe('ProgressInitiateFormComponent', () => { component.startTestRun(); expect(testRunServiceMock.startTestrun).toHaveBeenCalledWith({ + status: DeviceStatus.VALID, manufacturer: 'Delta', model: 'O3-DIN-CPU', mac_addr: '00:1e:42:35:73:c4', @@ -214,7 +210,7 @@ describe('ProgressInitiateFormComponent', () => { connection: { enabled: true, }, - dns: { + udmi: { enabled: true, }, }, @@ -240,18 +236,19 @@ describe('ProgressInitiateFormComponent', () => { expect(buttonSpy).toHaveBeenCalled(); }); - it('should focus firmware', () => { + it('should focus firmware', fakeAsync(() => { component.selectedDevice = device; component.setFirmwareFocus = true; const firmwareSpy = spyOn( - component.firmwareInput.nativeElement, + component.firmwareInput().nativeElement, 'focus' ); component.ngAfterViewChecked(); fixture.detectChanges(); + tick(100); expect(firmwareSpy).toHaveBeenCalled(); - }); + })); }); it('should focus element on focusButton ', () => { @@ -313,12 +310,6 @@ describe('ProgressInitiateFormComponent', () => { expect(deviceItem).toBeTruthy(); }); - it('should have tabindex -1 for device item', () => { - const deviceItem = compiled.querySelector('app-device-item button'); - - expect((deviceItem as HTMLElement).tabIndex).toBe(-1); - }); - it('should display firmware if device selected', () => { const firmware = compiled.querySelector('input'); diff --git a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.ts b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.ts index c1a2afb92..cb702c1b9 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.ts +++ b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.ts @@ -18,63 +18,110 @@ import { ChangeDetectorRef, Component, ElementRef, - Inject, OnInit, - ViewChild, + viewChild, + inject, } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog'; import { TestRunService } from '../../../../services/test-run.service'; -import { Device, TestModule, DeviceView } from '../../../../model/device'; +import { + Device, + TestModule, + DeviceStatus, + DeviceView, +} from '../../../../model/device'; import { AbstractControl, FormArray, FormBuilder, FormGroup, + ReactiveFormsModule, } from '@angular/forms'; import { DeviceValidators } from '../../../devices/components/device-form/device.validators'; import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; -import { take } from 'rxjs'; +import { take, timer } from 'rxjs'; import { Store } from '@ngrx/store'; import { AppState } from '../../../../store/state'; import { selectDevices } from '../../../../store/selectors'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { TestrunStatus } from '../../../../model/testrun-status'; +import { CalloutType } from '../../../../model/callout-type'; +import { CommonModule } from '@angular/common'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { DeviceTestsComponent } from '../../../../components/device-tests/device-tests.component'; +import { DeviceItemComponent } from '../../../../components/device-item/device-item.component'; +import { SpinnerComponent } from '../../../../components/spinner/spinner.component'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatButtonModule } from '@angular/material/button'; +import { CalloutComponent } from '../../../../components/callout/callout.component'; interface DialogData { device?: Device; + testModules: TestModule[]; } @Component({ selector: 'app-testrun-initiate-form', templateUrl: './testrun-initiate-form.component.html', styleUrls: ['./testrun-initiate-form.component.scss'], + imports: [ + CommonModule, + MatButtonModule, + MatIconModule, + MatToolbarModule, + MatProgressBarModule, + MatDialogModule, + DeviceItemComponent, + MatInputModule, + MatExpansionModule, + ReactiveFormsModule, + DeviceTestsComponent, + SpinnerComponent, + CalloutComponent, + MatTooltipModule, + ], }) export class TestrunInitiateFormComponent extends EscapableDialogComponent implements OnInit, AfterViewChecked { - @ViewChild('firmwareInput') firmwareInput!: ElementRef; + private startRequestSent = new BehaviorSubject(false); + override dialogRef: MatDialogRef; + data = inject(MAT_DIALOG_DATA); + private readonly testRunService = inject(TestRunService); + private fb = inject(FormBuilder); + private deviceValidators = inject(DeviceValidators); + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private store = inject>(Store); + + readonly firmwareInput = viewChild.required('firmwareInput'); initiateForm!: FormGroup; devices$ = this.store.select(selectDevices); selectedDevice: Device | null = null; testModules: TestModule[] = []; prevDevice: Device | null = null; setFirmwareFocus = false; + readonly DeviceStatus = DeviceStatus; readonly DeviceView = DeviceView; + public readonly CalloutType = CalloutType; error$: BehaviorSubject = new BehaviorSubject( null ); - constructor( - public override dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: DialogData, - private readonly testRunService: TestRunService, - private fb: FormBuilder, - private deviceValidators: DeviceValidators, - private readonly changeDetectorRef: ChangeDetectorRef, - private store: Store - ) { - super(dialogRef); + constructor() { + const dialogRef = + inject>(MatDialogRef); + + super(); + this.dialogRef = dialogRef; } get firmware() { @@ -91,7 +138,7 @@ export class TestrunInitiateFormComponent ngOnInit() { this.createInitiateForm(); - this.testModules = this.testRunService.getTestModules(); + this.testModules = this.data?.testModules; if (this.data?.device) { this.deviceSelected(this.data.device); @@ -120,9 +167,11 @@ export class TestrunInitiateFormComponent this.changeDetectorRef.detectChanges(); } if (this.setFirmwareFocus) { - this.firmwareInput?.nativeElement.focus(); - this.setFirmwareFocus = false; - this.changeDetectorRef.detectChanges(); + timer(100).subscribe(() => { + this.firmwareInput()?.nativeElement.focus(); + this.setFirmwareFocus = false; + this.changeDetectorRef.detectChanges(); + }); } } @@ -156,7 +205,8 @@ export class TestrunInitiateFormComponent } ); - if (this.selectedDevice) { + if (this.selectedDevice && !this.startRequestSent.value) { + this.startRequestSent.next(true); this.testRunService.fetchVersion(); this.testRunService .startTestrun({ @@ -165,8 +215,13 @@ export class TestrunInitiateFormComponent test_modules: testModules, }) .pipe(take(1)) - .subscribe(status => { - this.cancel(status); + .subscribe({ + next: status => { + this.cancel(status); + }, + error: () => { + this.startRequestSent.next(false); + }, }); } } diff --git a/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.html b/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.html index b94c0d55a..a459e1744 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.html +++ b/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.html @@ -16,17 +16,44 @@