diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 4071860f5..dcc5a2cfe 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -7,12 +7,13 @@ on: jobs: testrun_baseline: + permissions: {} name: Baseline runs-on: ubuntu-20.04 timeout-minutes: 20 steps: - name: Checkout source - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v4.1.1 - name: Install dependencies shell: bash {0} run: cmd/prepare @@ -28,13 +29,14 @@ jobs: run: testing/baseline/test_baseline testrun_tests: + permissions: {} name: Tests runs-on: ubuntu-20.04 needs: testrun_baseline timeout-minutes: 45 steps: - name: Checkout source - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v4.1.1 - name: Install dependencies shell: bash {0} run: cmd/prepare @@ -51,20 +53,21 @@ jobs: if: ${{ always() }} run: sudo tar --exclude-vcs -czf runtime.tgz /usr/local/testrun/runtime/ - name: Upload runtime results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ always() }} with: if-no-files-found: error - name: runtime_${{ github.workflow }}_${{ github.run_id }} + name: runtime_tests_${{ github.run_id }} path: runtime.tgz testrun_api: + permissions: {} name: API runs-on: ubuntu-20.04 timeout-minutes: 40 steps: - name: Checkout source - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v4.1.1 - name: Install dependencies shell: bash {0} run: cmd/prepare @@ -82,47 +85,50 @@ jobs: if: ${{ always() }} run: sudo tar --exclude-vcs -czf runtime.tgz /usr/local/testrun/runtime/ /usr/local/testrun/local/ - name: Upload runtime results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ always() }} with: if-no-files-found: error - name: runtime_${{ github.workflow }}_${{ github.run_id }} + name: runtime_api_${{ github.run_id }} path: runtime.tgz pylint: + permissions: {} name: Pylint runs-on: ubuntu-22.04 timeout-minutes: 5 steps: - name: Checkout source - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v4.1.1 - name: Run pylint shell: bash {0} run: testing/pylint/test_pylint testrun_package: + permissions: {} name: Package runs-on: ubuntu-22.04 timeout-minutes: 5 steps: - name: Checkout source - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v4.1.1 - name: Package Testrun shell: bash {0} run: cmd/package - name: Archive package - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: Testrun Installer + name: testrun_installer path: testrun*.deb testrun_ui: + permissions: {} name: UI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4.1.1 - name: Install Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18.10.0 - name: Install Chromium Browser diff --git a/.gitignore b/.gitignore index 3f944ba34..bf3d0c7d1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ pylint.out __pycache__/ build/ testing/unit_test/temp/ -*.deb \ No newline at end of file +*.deb + +make/DEBIAN/postinst \ No newline at end of file diff --git a/cmd/install b/cmd/install index 0b6ac92de..c4f53b905 100755 --- a/cmd/install +++ b/cmd/install @@ -28,6 +28,13 @@ pip3 install -r framework/requirements.txt # Copy the default configuration cp -n local/system.json.example local/system.json +# Set file permissions +# This does not work on GitHub actions +if logname ; then + USER_NAME=$(logname) + sudo chown "$USER_NAME" local/system.json +fi + deactivate # Build docker images diff --git a/cmd/package b/cmd/package index 87e66d977..7084343d6 100755 --- a/cmd/package +++ b/cmd/package @@ -53,4 +53,4 @@ cp -r {framework,modules} $MAKE_SRC_DIR/usr/local/testrun dpkg-deb --build --root-owner-group make # Rename the .deb file -mv make.deb testrun_1-1_amd64.deb +mv make.deb testrun_1-1-1_amd64.deb diff --git a/docs/get_started.md b/docs/get_started.md index 70215ac62..b7165d930 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -49,9 +49,10 @@ However, to achieve a compliant test outcome, your device must be configured cor - 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. - **NOTE: The device under test should be powered off until prompted** - - **NOTE: Both adapters should be disabled in the host system (IPv4, IPv6 and general). You can do this by going to Settings > Network** + Some things to remember: + - The device under test should be powered off until prompted + - Both adapters should be disabled in the host system (IPv4, IPv6 and general). You can do this by going to Settings > Network + - Struggling to identify the correct interfaces? See [this guide](network/identify_interfaces.md). 2. Start Testrun. diff --git a/docs/network/README.md b/docs/network/README.md index 2d66d3e6a..601ed1349 100644 --- a/docs/network/README.md +++ b/docs/network/README.md @@ -2,8 +2,9 @@ ## Table of Contents 1) Network Overview (this page) -2) [Addresses](addresses.md) -3) [Add a new network service](add_new_service.md) +2) [How to identify network interfaces](identify_interfaces.md) +3) [Addresses](addresses.md) +4) [Add a new network service](add_new_service.md) Test Run 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. diff --git a/docs/network/identify_interfaces.md b/docs/network/identify_interfaces.md new file mode 100644 index 000000000..6baddb6ca --- /dev/null +++ b/docs/network/identify_interfaces.md @@ -0,0 +1,18 @@ +# 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/framework/python/src/api/api.py b/framework/python/src/api/api.py index 305310e7e..ebd022acf 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -11,7 +11,7 @@ # 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 Testrun data via REST API.""" from fastapi import FastAPI, APIRouter, Response, Request, status from fastapi.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware @@ -37,7 +37,8 @@ DEVICES_PATH = "/usr/local/testrun/local/devices" DEFAULT_DEVICE_INTF = "enx123456789123" -LATEST_RELEASE_CHECK = "https://api.github.com/repos/google/testrun/releases/latest" +LATEST_RELEASE_CHECK = ("https://api.github.com/repos/google/" + + "testrun/releases/latest") class Api: """Provide REST endpoints to manage Testrun""" @@ -77,6 +78,9 @@ def __init__(self, test_run): self.delete_device, methods=["DELETE"]) self._router.add_api_route("/device", self.save_device, methods=["POST"]) + self._router.add_api_route("/device/edit", + self.edit_device, + methods=["POST"]) # Allow all origins to access the API origins = ["*"] @@ -111,14 +115,17 @@ def stop(self): async def get_sys_interfaces(self): addrs = psutil.net_if_addrs() - ifaces = [] - for iface in addrs: + ifaces = {} + + # pylint: disable=consider-using-dict-items + for key in addrs.keys(): + nic = addrs[key] # Ignore any interfaces that are not ethernet - if not (iface.startswith("en") or iface.startswith("eth")): + if not (key.startswith("en") or key.startswith("eth")): continue - ifaces.append(iface) + ifaces[key] = nic[0].address return ifaces @@ -240,32 +247,38 @@ async def get_version(self, response: Response): json_response["installed_version"] = "v" + current_version # Check latest version number from GitHub API - version_check = requests.get(LATEST_RELEASE_CHECK, timeout=5) + try: + version_check = requests.get(LATEST_RELEASE_CHECK, timeout=5) + + # Check OK response was received + if version_check.status_code != 200: + response.status_code = 500 + LOGGER.error(version_check.content) + return self._generate_msg(False, "Failed to fetch latest version") + + # Extract version number from response, removing the leading 'v' + latest_version_no = version_check.json()["name"].strip("v") + LOGGER.debug(f"Latest version available is {latest_version_no}") + + # Craft JSON response + json_response["latest_version"] = "v" + latest_version_no + json_response["latest_version_url"] = version_check.json()["html_url"] + + # String comparison between current and latest version + if latest_version_no > current_version: + json_response["update_available"] = True + LOGGER.debug("An update is available") + else: + json_response["update_available"] = False + LOGGER.debug("The latest version is installed") - # Check OK response was received - if version_check.status_code != 200: + return json_response + except Exception as e: response.status_code = 500 - LOGGER.error(version_check.content) + LOGGER.error("Failed to fetch latest version") + LOGGER.debug(e) return self._generate_msg(False, "Failed to fetch latest version") - # Extract version number from response, removing the leading 'v' - latest_version_no = version_check.json()["name"].strip("v") - LOGGER.debug(f"Latest version available is {latest_version_no}") - - # Craft JSON response - json_response["latest_version"] = "v" + latest_version_no - json_response["latest_version_url"] = version_check.json()["html_url"] - - # String comparison between current and latest version - if latest_version_no > current_version: - json_response["update_available"] = True - LOGGER.debug("An update is available") - else: - json_response["update_available"] = False - LOGGER.debug("The latest version is installed") - - return json_response - async def get_reports(self, request: Request): LOGGER.debug("Received reports list request") # Resolve the server IP from the request so we @@ -389,8 +402,76 @@ async def save_device(self, request: Request, response: Response): else: - self._test_run.save_device(device, device_json) - response.status_code = status.HTTP_200_OK + response.status_code = status.HTTP_409_CONFLICT + return self._generate_msg(False, "A device with that " + + "MAC address already exists") + + return device.to_config_json() + + # Catch JSON Decode error etc + except JSONDecodeError: + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg(False, "Invalid JSON received") + + async def edit_device(self, request: Request, response: Response): + + LOGGER.debug("Received device edit request") + + try: + req_raw = (await request.body()).decode("UTF-8") + req_json = json.loads(req_raw) + + # Validate top level fields + if not (DEVICE_MAC_ADDR_KEY in req_json and + "device" in req_json): + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg(False, "Invalid request received") + + # Extract device information from request + device_json = req_json.get("device") + + if not self._validate_device_json(device_json): + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg(False, "Invalid request received") + + # Get device from old MAC address + device = self._session.get_device(req_json.get(DEVICE_MAC_ADDR_KEY)) + + # Check if device exists + 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") + + if (self._session.get_target_device() == device and + self._session.get_status() not in [ + "Cancelled", + "Compliant", + "Non-Compliant"]): + response.status_code = 403 + return self._generate_msg(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( + device_json.get(DEVICE_MAC_ADDR_KEY)) + + if not check_new_device is None and (device.mac_addr + != 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") + + # 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_modules = device_json.get(DEVICE_TEST_MODULES_KEY) + + self._test_run.save_device(device, device_json) + response.status_code = status.HTTP_200_OK return device.to_config_json() @@ -399,6 +480,7 @@ async def save_device(self, request: Request, response: Response): response.status_code = status.HTTP_400_BAD_REQUEST return self._generate_msg(False, "Invalid JSON received") + async def get_report(self, response: Response, device_name, timestamp): diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index 70442e9ff..c9ce524c3 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -42,6 +42,7 @@ def __init__(self, config_file): self._runtime_params = [] self._device_repository = [] self._total_tests = 0 + self._report_url = None self._version = None self._load_version() @@ -231,9 +232,16 @@ def add_total_tests(self, no_tests): def get_total_tests(self): return self._total_tests + def get_report_url(self): + return self._report_url + + def set_report_url(self, url): + self._report_url = url + def reset(self): self.set_status('Idle') self.set_target_device(None) + self._report_url = None self._total_tests = 0 self._results = [] self._started = None @@ -241,8 +249,6 @@ def reset(self): def to_json(self): - # TODO: Add report URL - results = { 'total': self.get_total_tests(), 'results': self.get_test_results() @@ -256,6 +262,9 @@ def to_json(self): 'tests': results } + if self._report_url is not None: + session_json['report'] = self.get_report_url() + return session_json def get_timezone(self): diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py index 0ac4cb9a6..25df0e511 100644 --- a/framework/python/src/common/testreport.py +++ b/framework/python/src/common/testreport.py @@ -75,6 +75,12 @@ def get_duration(self): def add_test(self, test): self._results.append(test) + def set_report_url(self, url): + self._report_url = url + + def get_report_url(self): + return self._report_url + def to_json(self): report_json = {} report_json['device'] = self._device @@ -182,7 +188,7 @@ def generate_pages(self, json_data): def generate_page(self, json_data, page_num, max_page): # Placeholder until available in json report - version = 'v1.1 (2023-12-15)' + version = 'v1.1.1 (2024-01-31)' page = '
' page += self.generate_header(json_data) if page_num == 1: diff --git a/framework/python/src/common/util.py b/framework/python/src/common/util.py index 098c478d3..ebc3d3d70 100644 --- a/framework/python/src/common/util.py +++ b/framework/python/src/common/util.py @@ -32,21 +32,21 @@ def run_command(cmd, output=True): by any return code from the process other than zero.""" success = False - process = subprocess.Popen(shlex.split(cmd), + with subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - 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 - if output: - return stdout.strip().decode('utf-8'), stderr - else: - return success + 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 + if output: + return stdout.strip().decode('utf-8'), stderr + else: + return success def interface_exists(interface): diff --git a/framework/python/src/core/testrun.py b/framework/python/src/core/testrun.py index 48e31c721..f579eba26 100644 --- a/framework/python/src/core/testrun.py +++ b/framework/python/src/core/testrun.py @@ -36,6 +36,8 @@ 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__)) @@ -430,16 +432,22 @@ def start_ui(self): client = docker.from_env() - client.containers.run( - image='test-run/ui', - auto_remove=True, - name='tr-ui', - hostname='testrun.io', - detach=True, - ports={ - '80': 8080 - } - ) + 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. ' + + 'Please investigate and try again.') + LOGGER.error(ie) + sys.exit(1) # TODO: Make port configurable LOGGER.info('User interface is ready on http://localhost:8080') diff --git a/framework/python/src/net_orc/network_validator.py b/framework/python/src/net_orc/network_validator.py index 3866bd3ae..6673a1fdb 100644 --- a/framework/python/src/net_orc/network_validator.py +++ b/framework/python/src/net_orc/network_validator.py @@ -240,7 +240,8 @@ def _attach_device_to_network(self, device): mac_addr = TR_CONTAINER_MAC_PREFIX + '10' - util.run_command('ip link set dev ' + container_intf + ' address ' + mac_addr) + util.run_command('ip link set dev ' + container_intf + + ' address ' + mac_addr) # Add bridge interface to device bridge util.run_command('ovs-vsctl add-port ' + DEVICE_BRIDGE + ' ' + bridge_intf) diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py index 21b8650bb..d9b25108d 100644 --- a/framework/python/src/test_orc/test_orchestrator.py +++ b/framework/python/src/test_orc/test_orchestrator.py @@ -115,6 +115,7 @@ def run_test_modules(self): self._write_reports(report) self._test_in_progress = False self._timestamp_results(device) + self.get_session().set_report_url(report.get_report_url()) LOGGER.debug("Cleaning old test results...") self._cleanup_old_test_results(device) @@ -234,6 +235,7 @@ def _timestamp_results(self, device): device.mac_addr.replace(":", "") ) + # Define the directory completed_results_dir = os.path.join( self._root_path, LOCAL_DEVICE_REPORTS.replace("{device_folder}", device.device_folder), @@ -246,6 +248,8 @@ def _timestamp_results(self, device): shutil.copytree(cur_results_dir, completed_results_dir, dirs_exist_ok=True) util.run_command(f"chown -R {self._host_user} '{completed_results_dir}'") + return completed_results_dir + def test_in_progress(self): return self._test_in_progress diff --git a/make/DEBIAN/control b/make/DEBIAN/control index c5af91918..6fa63afeb 100644 --- a/make/DEBIAN/control +++ b/make/DEBIAN/control @@ -1,5 +1,5 @@ Package: Testrun -Version: 1.1 +Version: 1.1.1 Architecture: amd64 Maintainer: Google Homepage: https://github.com/google/testrun diff --git a/make/DEBIAN/postinst b/make/DEBIAN/postinst deleted file mode 100755 index 0b6ac92de..000000000 --- a/make/DEBIAN/postinst +++ /dev/null @@ -1,36 +0,0 @@ -#!/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. - -echo Installing application dependencies - -TESTRUN_DIR=/usr/local/testrun -cd $TESTRUN_DIR - -python3 -m venv venv - -source venv/bin/activate - -pip3 install -r framework/requirements.txt - -# Copy the default configuration -cp -n local/system.json.example local/system.json - -deactivate - -# Build docker images -sudo cmd/build - -echo Finished installing Testrun diff --git a/modules/network/base/bin/capture b/modules/network/base/bin/capture index 59ffb4118..224d4e0c5 100644 --- a/modules/network/base/bin/capture +++ b/modules/network/base/bin/capture @@ -38,7 +38,7 @@ fi # Create the output directory and start the capture mkdir -p $PCAP_DIR chown $HOST_USER $PCAP_DIR -tcpdump -i $INTERFACE -w $PCAP_DIR/$PCAP_FILE -Z $HOST_USER & +tcpdump -U -i $INTERFACE -w $PCAP_DIR/$PCAP_FILE -Z $HOST_USER & #Small pause to let the capture to start sleep 1 \ No newline at end of file diff --git a/modules/network/dhcp-1/dhcp-1.Dockerfile b/modules/network/dhcp-1/dhcp-1.Dockerfile index 8fc2d0630..e50ed9a95 100644 --- a/modules/network/dhcp-1/dhcp-1.Dockerfile +++ b/modules/network/dhcp-1/dhcp-1.Dockerfile @@ -24,9 +24,6 @@ RUN apt-get update --fix-missing # Install all necessary packages RUN apt-get install -y wget -# Update the oui.txt file from ieee -RUN wget http://standards-oui.ieee.org/oui.txt -P /usr/local/etc/ - # Install dhcp server RUN apt-get install -y isc-dhcp-server radvd systemd diff --git a/modules/network/dhcp-2/dhcp-2.Dockerfile b/modules/network/dhcp-2/dhcp-2.Dockerfile index 58256813f..66ea857c3 100644 --- a/modules/network/dhcp-2/dhcp-2.Dockerfile +++ b/modules/network/dhcp-2/dhcp-2.Dockerfile @@ -18,15 +18,12 @@ FROM test-run/base:latest ARG MODULE_NAME=dhcp-2 ARG MODULE_DIR=modules/network/$MODULE_NAME -#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 # Install all necessary packages RUN apt-get install -y wget -# Update the oui.txt file from ieee -RUN wget http://standards-oui.ieee.org/oui.txt -P /usr/local/etc/ - # Install dhcp server RUN apt-get install -y isc-dhcp-server radvd systemd diff --git a/modules/test/base/base.Dockerfile b/modules/test/base/base.Dockerfile index 340d0e983..878273055 100644 --- a/modules/test/base/base.Dockerfile +++ b/modules/test/base/base.Dockerfile @@ -22,7 +22,7 @@ ARG COMMON_DIR=framework/python/src/common RUN apt-get update # Install common software -RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq net-tools iputils-ping tzdata tcpdump iproute2 jq python3 python3-pip dos2unix nmap --fix-missing +RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq net-tools iputils-ping tzdata tcpdump iproute2 jq python3 python3-pip dos2unix nmap wget --fix-missing # Install common python modules COPY $COMMON_DIR/ /testrun/python/src/common @@ -50,5 +50,8 @@ 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/ +# Update the oui.txt file from ieee +RUN wget https://standards-oui.ieee.org/oui/oui.txt -P /usr/local/etc/ + # Start the test module ENTRYPOINT [ "/testrun/bin/start" ] \ No newline at end of file diff --git a/modules/test/base/bin/capture b/modules/test/base/bin/capture index 69fa916c3..180b447cf 100644 --- a/modules/test/base/bin/capture +++ b/modules/test/base/bin/capture @@ -27,7 +27,7 @@ INTERFACE=$2 # Create the output directory and start the capture mkdir -p $PCAP_DIR chown $HOST_USER $PCAP_DIR -tcpdump -i $INTERFACE -w $PCAP_DIR/$PCAP_FILE -Z $HOST_USER & +tcpdump -U -i $INTERFACE -w $PCAP_DIR/$PCAP_FILE -Z $HOST_USER & # Small pause to let the capture to start sleep 1 \ No newline at end of file diff --git a/modules/test/conn/conn.Dockerfile b/modules/test/conn/conn.Dockerfile index af1921d69..a9f523e44 100644 --- a/modules/test/conn/conn.Dockerfile +++ b/modules/test/conn/conn.Dockerfile @@ -23,13 +23,10 @@ ARG GRPC_PROTO_FILE="grpc.proto" # Install all necessary packages RUN apt-get install -y wget -# Update the oui.txt file from ieee -RUN wget http://standards-oui.ieee.org/oui.txt -P /usr/local/etc/ - -#Load the requirements file +# Load the requirements file COPY $MODULE_DIR/python/requirements.txt /testrun/python -#Install all python requirements for the module +# Install all python requirements for the module RUN pip3 install -r /testrun/python/requirements.txt # Copy over all configuration files diff --git a/modules/test/conn/python/requirements.txt b/modules/test/conn/python/requirements.txt index c2275b3e0..7244e9e75 100644 --- a/modules/test/conn/python/requirements.txt +++ b/modules/test/conn/python/requirements.txt @@ -1,2 +1,3 @@ pyOpenSSL -scapy \ No newline at end of file +scapy +python-dateutil \ No newline at end of file diff --git a/modules/test/conn/python/src/connection_module.py b/modules/test/conn/python/src/connection_module.py index 3fc0e6765..9429541de 100644 --- a/modules/test/conn/python/src/connection_module.py +++ b/modules/test/conn/python/src/connection_module.py @@ -14,7 +14,6 @@ """Connection test module""" import util import time -import datetime import traceback from scapy.all import rdpcap, DHCP, Ether, IPv6, ICMPv6ND_NS from test_module import TestModule @@ -85,7 +84,8 @@ def _connection_shared_address(self, config): def _connection_dhcp_address(self): LOGGER.info('Running connection.dhcp_address') - lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, timeout=self._lease_wait_time_sec) + lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, + timeout=self._lease_wait_time_sec) if lease is not None: if 'ip' in lease: ip_addr = lease['ip'] @@ -176,7 +176,7 @@ def _connection_target_ping(self): else: return False, 'Device does not respond to ping' - def _connection_ipaddr_ip_change(self,config): + def _connection_ipaddr_ip_change(self, config): result = None LOGGER.info('Running connection.ipaddr.ip_change') # Resolve the configured lease wait time @@ -184,7 +184,8 @@ def _connection_ipaddr_ip_change(self,config): self._lease_wait_time_sec = config['lease_wait_time_sec'] if self._dhcp_util.setup_single_dhcp_server(): - lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, timeout=self._lease_wait_time_sec) + 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') LOGGER.debug(str(lease)) @@ -192,7 +193,8 @@ def _connection_ipaddr_ip_change(self,config): ip_address = '10.10.10.30' if self._dhcp_util.add_reserved_lease(lease['hostname'], lease['hw_addr'], ip_address): - self._dhcp_util.wait_for_lease_expire(lease) + self._dhcp_util.wait_for_lease_expire(lease, + self._lease_wait_time_sec) LOGGER.info('Checking device accepted new ip') for _ in range(5): LOGGER.info('Pinging device at IP: ' + ip_address) @@ -216,12 +218,13 @@ def _connection_ipaddr_ip_change(self,config): 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) + self._dhcp_util.get_cur_lease(mac_address=self._device_mac, + timeout=self._lease_wait_time_sec) else: result = None, 'Failed to configure network for test' return result - def _connection_ipaddr_dhcp_failover(self,config): + def _connection_ipaddr_dhcp_failover(self, config): result = None LOGGER.info('Running connection.ipaddr.dhcp_failover') @@ -235,17 +238,20 @@ def _connection_ipaddr_dhcp_failover(self,config): secondary_status = self._dhcp_util.get_dhcp_server_status( dhcp_server_primary=False) if primary_status and secondary_status: - lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, timeout=self._lease_wait_time_sec) + 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): # Shutdown the primary server if self._dhcp_util.stop_dhcp_server(dhcp_server_primary=True): # Wait until the current lease is expired - self._dhcp_util.wait_for_lease_expire(lease) + self._dhcp_util.wait_for_lease_expire(lease, + self._lease_wait_time_sec) # Make sure the device has received a new lease from the # secondary server - if self._dhcp_util.get_cur_lease(mac_address=self._device_mac,timeout=self._lease_wait_time_sec): + 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, ('Secondary DHCP server lease confirmed active ' 'in device') @@ -294,9 +300,8 @@ def _connection_ipv6_slaac(self): return result def _has_slaac_addres(self): - packet_capture = (rdpcap(STARTUP_CAPTURE_FILE) - + rdpcap(MONITOR_CAPTURE_FILE) - + rdpcap(DHCP_CAPTURE_FILE)) + packet_capture = (rdpcap(STARTUP_CAPTURE_FILE) + + rdpcap(MONITOR_CAPTURE_FILE) + rdpcap(DHCP_CAPTURE_FILE)) sends_ipv6 = False for packet_number, packet in enumerate(packet_capture, start=1): if IPv6 in packet and packet.src == self._device_mac: @@ -366,7 +371,8 @@ def setup_single_dhcp_server(self): if response.code == 200: LOGGER.info('Secondary DHCP server stopped') LOGGER.info('Configuring primary DHCP server') - # Move primary DHCP server from failover into a single DHCP server config + # Move primary DHCP server from failover into + # a single DHCP server config response = self.dhcp1_client.disable_failover() if response.code == 200: LOGGER.info('Primary DHCP server failover disabled') @@ -410,7 +416,7 @@ def _run_subnet_test(self, config): # Resolve the configured lease wait time if 'lease_wait_time_sec' in config: - self._lease_wait_time_sec = config['lease_wait_time_sec'] + self._lease_wait_time_sec = config['lease_wait_time_sec'] response = self.dhcp1_client.get_dhcp_range() cur_range = {} @@ -428,12 +434,13 @@ def _run_subnet_test(self, config): 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) + 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") + LOGGER.info('Failed to confirm a valid active lease for the device') return None, 'Failed to confirm a valid active lease for the device' else: LOGGER.error(dhcp_setup[1]) @@ -459,15 +466,18 @@ def _run_subnet_test(self, config): self.restore_failover_dhcp_server(cur_range) # Wait for the current lease to expire - self._wait_for_lease_expire(self._dhcp_util.get_cur_lease( - self._device_mac,timeout=self._lease_wait_time_sec)) + 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) # 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 - lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac,timeout=2*self._lease_wait_time_sec) + lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, + timeout=2 * + self._lease_wait_time_sec) if lease is not None: LOGGER.info('Validating subnet for new lease...') in_range = self.is_ip_in_range(lease['ip'], cur_range['start'], @@ -482,47 +492,28 @@ def _run_subnet_test(self, config): return final_result, final_result_details def _test_subnet(self, subnet, lease): - LOGGER.info("Testing subnet: " + str(subnet)) + LOGGER.info('Testing subnet: ' + str(subnet)) if self._change_subnet(subnet): - expiration = datetime.datetime.strptime( - lease['expires'], '%Y-%m-%d %H:%M:%S') - now_utc = datetime.datetime.now( - datetime.timezone.utc).replace(tzinfo=None) - time_to_expire = (expiration - now_utc).total_seconds() - LOGGER.debug('Time until lease expiration: ' + str(time_to_expire)) - LOGGER.info('Waiting for current lease to expire: ' + str(expiration)) - if time_to_expire > 0: - # Wait until the expiration time and add 5 seconds - time.sleep(time_to_expire + 5) - LOGGER.debug('Current lease expired. Checking for new lease') - LOGGER.debug('Checking for new lease') - # Subnet changes tend to take longer to pick up so we'll allow - # for twice the lease wait time - lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac,timeout=2*self._lease_wait_time_sec) - if lease is not None: - LOGGER.debug('New lease found: ' + str(lease)) - LOGGER.debug('Validating subnet for new lease...') - in_range = self.is_ip_in_range(lease['ip'], subnet['start'], - subnet['end']) - LOGGER.info('Lease within subnet: ' + str(in_range)) - return in_range - else: - LOGGER.info('Device did not receive lease in subnet') - return False + self._dhcp_util.wait_for_lease_expire(lease, self._lease_wait_time_sec) + LOGGER.debug('Checking for new lease') + # Subnet changes tend to take longer to pick up so we'll allow + # for twice the lease wait time + lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, + timeout=2 * + self._lease_wait_time_sec) + if lease is not None: + LOGGER.debug('New lease found: ' + str(lease)) + LOGGER.debug('Validating subnet for new lease...') + in_range = self.is_ip_in_range(lease['ip'], subnet['start'], + subnet['end']) + LOGGER.info('Lease within subnet: ' + str(in_range)) + return in_range + else: + LOGGER.info('Device did not receive lease in subnet') + return False else: LOGGER.error('Failed to change subnet') - def _wait_for_lease_expire(self, lease): - expiration = datetime.datetime.strptime( - lease['expires'], '%Y-%m-%d %H:%M:%S') - time_to_expire = expiration - datetime.datetime.now() - LOGGER.info('Time until lease expiration: ' + str(time_to_expire)) - LOGGER.info('Waiting for current lease to expire: ' + str(expiration)) - if time_to_expire.total_seconds() > 0: - time.sleep(time_to_expire.total_seconds() + - 5) # Wait until the expiration time and padd 5 seconds - LOGGER.info('Current lease expired.') - def _change_subnet(self, subnet): LOGGER.info('Changing subnet to: ' + str(subnet)) response = self.dhcp1_client.set_dhcp_range(subnet['start'], subnet['end']) @@ -549,25 +540,25 @@ def test_subnets(self, subnets): result = self._test_subnet(subnet, lease) if result: result = { - 'result': - True, - 'details': - 'Subnet ' + subnet['start'] + '-' + subnet['end'] + ' passed' + 'result': + True, + 'details': + 'Subnet ' + subnet['start'] + '-' + subnet['end'] + ' passed' } else: result = { - 'result': - False, - 'details': - 'Subnet ' + subnet['start'] + '-' + subnet['end'] + ' failed' + 'result': + False, + 'details': + 'Subnet ' + subnet['start'] + '-' + subnet['end'] + ' failed' } else: result = { - 'result': - None, - 'details': - 'Device does not have active lease, cannot test subnet change. ' + - 'Subnet ' + subnet['start'] + '-' + subnet['end'] + ' skipped' + 'result': + None, + 'details': + 'Device does not have active lease, cannot test subnet change. ' + + 'Subnet ' + subnet['start'] + '-' + subnet['end'] + ' skipped' } except Exception as e: # pylint: disable=W0718 LOGGER.error('Subnet test failed: ' + str(e)) diff --git a/modules/test/conn/python/src/dhcp_util.py b/modules/test/conn/python/src/dhcp_util.py index 6d063d62a..be5f0cac2 100644 --- a/modules/test/conn/python/src/dhcp_util.py +++ b/modules/test/conn/python/src/dhcp_util.py @@ -17,6 +17,7 @@ import time from datetime import datetime import util +from dateutil import tz LOG_NAME = 'dhcp_util' LOGGER = None @@ -125,17 +126,22 @@ def get_cur_lease(self, mac_address, timeout): Retrieve the current lease for a given MAC address with retries. Args: - mac_address (str): The MAC address of the client whose lease is being queried. - timeout (int): The maximum time (in seconds) to wait for a lease to be found. + mac_address (str): The MAC address of the client whose + lease is being queried. + timeout (int): The maximum time (in seconds) to wait + for a lease to be found. Returns: - str or None: The lease information as a string if found, or None if no lease is found within the timeout. + str or None: The lease information as a string if found, + or None if no lease is found within the timeout. Note: - This method will attempt to query both primary and secondary DHCP servers for the lease, - with a 5-second pause between retries until the `timeout` is reached. + This method will attempt to query both primary and secondary + DHCP servers for the lease, with a 5-second pause between + retries until the `timeout` is reached. """ - LOGGER.info('Resolving current lease with max wait time of ' + str(timeout) + ' seconds') + LOGGER.info('Resolving current lease with max wait time of ' + + str(timeout) + ' seconds') start_time = time.time() while True: @@ -146,24 +152,27 @@ def get_cur_lease(self, mac_address, timeout): def _get_cur_lease(self, mac_address): """ - Retrieve the current lease for a given MAC address from both primary and secondary DHCP servers. + Retrieve the current lease for a given MAC address from both + primary and secondary DHCP servers. Args: - mac_address (str): The MAC address of the client whose lease is being queried. + mac_address (str): The MAC address of the client whose + lease is being queried. Returns: - str or None: The lease information as a string if found, or None if no lease is found. + str or None: The lease information as a string if found, + or None if no lease is found. """ primary = False lease = self._get_cur_lease_from_server(mac_address=mac_address, dhcp_server_primary=True) if lease is not None: - primary=True + primary = True else: lease = self._get_cur_lease_from_server(mac_address=mac_address, dhcp_server_primary=False) if lease is not None: - lease['primary']=primary + lease['primary'] = primary log_msg = 'DHCP lease resolved from ' log_msg += 'primary' if lease['primary'] else 'secondary' log_msg += ' server' @@ -176,7 +185,8 @@ def _get_cur_lease_from_server(self, mac_address, dhcp_server_primary=True): # Check if the server is online first, old lease files can still return # lease information that is no longer valid when a dhcp server is shutdown if self.get_dhcp_server_status(dhcp_server_primary): - response = self.get_dhcp_client(dhcp_server_primary).get_lease(mac_address) + response = self.get_dhcp_client(dhcp_server_primary).get_lease( + mac_address) if response.code == 200: lease_resp = eval(response.message) # pylint: disable=W0123 if lease_resp: # Check if non-empty lease @@ -245,12 +255,29 @@ def setup_single_dhcp_server(self): LOGGER.error('Failed to stop secondary DHCP server') return False - def wait_for_lease_expire(self, lease): - expiration = datetime.strptime(lease['expires'], '%Y-%m-%d %H:%M:%S') - time_to_expire = expiration - datetime.now() - LOGGER.info('Time until lease expiration: ' + str(time_to_expire)) + 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 time_to_expire.total_seconds() > 0: - time.sleep(time_to_expire.total_seconds() + - 5) # Wait until the expiration time and padd 5 seconds - LOGGER.info('Current lease expired.') + if wait_time > 0: + time.sleep(wait_time) + LOGGER.info('Current lease expired.') + + # Convert from a UTC datetime to the local time zone + def utc_to_local(self, utc_datetime): + # Set the time zone for the UTC datetime + utc = utc_datetime.replace(tzinfo=tz.tzutc()) + + # Convert to local time zone + local_datetime = utc.astimezone(tz.tzlocal()) + + return local_datetime diff --git a/modules/test/ntp/python/src/ntp_module.py b/modules/test/ntp/python/src/ntp_module.py index 0e7de00ec..40089ad44 100644 --- a/modules/test/ntp/python/src/ntp_module.py +++ b/modules/test/ntp/python/src/ntp_module.py @@ -36,7 +36,9 @@ def __init__(self, module): def _ntp_network_ntp_support(self): LOGGER.info('Running ntp.network.ntp_support') result = None - packet_capture = rdpcap(STARTUP_CAPTURE_FILE) + rdpcap(MONITOR_CAPTURE_FILE) + packet_capture = (rdpcap(STARTUP_CAPTURE_FILE) + + rdpcap(MONITOR_CAPTURE_FILE) + + rdpcap(NTP_SERVER_CAPTURE_FILE)) device_sends_ntp4 = False device_sends_ntp3 = False @@ -71,7 +73,9 @@ def _ntp_network_ntp_support(self): def _ntp_network_ntp_dhcp(self): LOGGER.info('Running ntp.network.ntp_dhcp') result = None - packet_capture = rdpcap(STARTUP_CAPTURE_FILE) + rdpcap(MONITOR_CAPTURE_FILE) + packet_capture = (rdpcap(STARTUP_CAPTURE_FILE) + + rdpcap(MONITOR_CAPTURE_FILE) + + rdpcap(NTP_SERVER_CAPTURE_FILE)) device_sends_ntp = False ntp_to_local = False diff --git a/modules/ui/package-lock.json b/modules/ui/package-lock.json index 8e90d3a71..f5a5b5e2f 100644 --- a/modules/ui/package-lock.json +++ b/modules/ui/package-lock.json @@ -8,44 +8,44 @@ "name": "test-run-ui", "version": "0.0.0", "dependencies": { - "@angular/animations": "^16.2.10", - "@angular/cdk": "^16.2.9", - "@angular/common": "^16.2.10", - "@angular/compiler": "^16.2.10", - "@angular/core": "^16.2.10", - "@angular/forms": "^16.2.10", - "@angular/material": "^16.2.9", - "@angular/platform-browser": "^16.2.10", - "@angular/platform-browser-dynamic": "^16.2.10", - "@angular/router": "^16.2.10", - "ngx-mask": "^16.3.9", + "@angular/animations": "^16.2.12", + "@angular/cdk": "^16.2.13", + "@angular/common": "^16.2.12", + "@angular/compiler": "^16.2.12", + "@angular/core": "^16.2.12", + "@angular/forms": "^16.2.12", + "@angular/material": "^16.2.13", + "@angular/platform-browser": "^16.2.12", + "@angular/platform-browser-dynamic": "^16.2.12", + "@angular/router": "^16.2.12", + "ngx-mask": "^16.4.2", "rxjs": "~7.8.0", "tslib": "^2.6.2", "zone.js": "~0.13.3" }, "devDependencies": { - "@angular-devkit/build-angular": "^16.2.6", + "@angular-devkit/build-angular": "^16.2.11", "@angular-eslint/builder": "16.2.0", "@angular-eslint/eslint-plugin": "16.2.0", "@angular-eslint/eslint-plugin-template": "16.2.0", "@angular-eslint/schematics": "16.2.0", "@angular-eslint/template-parser": "16.2.0", "@angular/cli": "~16.1.8", - "@angular/compiler-cli": "^16.2.10", + "@angular/compiler-cli": "^16.2.12", "@types/jasmine": "~4.3.6", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", - "eslint": "^8.49.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.1", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "jasmine-core": "~4.6.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", - "prettier": "^3.0.3", - "prettier-eslint": "^16.1.1", + "prettier": "^3.1.1", + "prettier-eslint": "^16.2.0", "typescript": "~5.1.3" } }, @@ -72,12 +72,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1602.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.6.tgz", - "integrity": "sha512-b1NNV3yNg6Rt86ms20bJIroWUI8ihaEwv5k+EoijEXLoMs4eNs5PhqL+QE8rTj+q9pa1gSrWf2blXor2JGwf1g==", + "version": "0.1602.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.11.tgz", + "integrity": "sha512-qC1tPL/82gxqCS1z9pTpLn5NQH6uqbV6UNjbkFEQpTwEyWEK6VLChAJsybHHfbpssPS2HWf31VoUzX7RqDjoQQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.6", + "@angular-devkit/core": "16.2.11", "rxjs": "7.8.1" }, "engines": { @@ -87,15 +87,15 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.6.tgz", - "integrity": "sha512-QdU/q77K1P8CPEEZGxw1QqLcnA9ofboDWS7vcLRBmFmk2zydtLTApbK0P8GNDRbnmROOKkoaLo+xUTDJz9gvPA==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.11.tgz", + "integrity": "sha512-yNzUiAeg1WHMsFG9IBg4S/7dsMcEAMYQ1I360ib80c0T/IwRb8pHhOokrl5Mu8zfNqZ/dxH4ItKY1uIMDmuMGQ==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1602.6", - "@angular-devkit/build-webpack": "0.1602.6", - "@angular-devkit/core": "16.2.6", + "@angular-devkit/architect": "0.1602.11", + "@angular-devkit/build-webpack": "0.1602.11", + "@angular-devkit/core": "16.2.11", "@babel/core": "7.22.9", "@babel/generator": "7.22.9", "@babel/helper-annotate-as-pure": "7.22.5", @@ -107,7 +107,7 @@ "@babel/runtime": "7.22.6", "@babel/template": "7.22.5", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "16.2.6", + "@ngtools/webpack": "16.2.11", "@vitejs/plugin-basic-ssl": "1.0.1", "ansi-colors": "4.1.3", "autoprefixer": "10.4.14", @@ -150,7 +150,7 @@ "text-table": "0.2.0", "tree-kill": "1.2.2", "tslib": "2.6.1", - "vite": "4.4.7", + "vite": "4.5.1", "webpack": "5.88.2", "webpack-dev-middleware": "6.1.1", "webpack-dev-server": "4.15.1", @@ -215,12 +215,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1602.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.6.tgz", - "integrity": "sha512-BJPR6xdq7gRJ6bVWnZ81xHyH75j7lyLbegCXbvUNaM8TWVBkwWsSdqr2NQ717dNLLn5umg58SFpU/pWMq6CxMQ==", + "version": "0.1602.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.11.tgz", + "integrity": "sha512-2Au6xRMxNugFkXP0LS1TwNE5gAfGW4g6yxC9P5j5p3kdGDnAVaZRTOKB9dg73i3uXtJHUMciYOThV0b78XRxwA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1602.6", + "@angular-devkit/architect": "0.1602.11", "rxjs": "7.8.1" }, "engines": { @@ -234,9 +234,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.6.tgz", - "integrity": "sha512-iez/8NYXQT6fqVQLlKmZUIRkFUEZ88ACKbTwD4lBmk0+hXW+bQBxI7JOnE3C4zkcM2YeuTXIYsC5SebTKYiR4Q==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.11.tgz", + "integrity": "sha512-u3cEQHqhSMWyAFIaPdRukCJwEUJt7Fy3C02gTlTeCB4F/OnftVFIm2e5vmCqMo9rgbfdvjWj9V+7wWiCpMrzAQ==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -443,9 +443,9 @@ } }, "node_modules/@angular/animations": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.10.tgz", - "integrity": "sha512-UudunZoyFWWNpuWkwiBxC3cleLCVJGHIfMgypFwC35YjtiIlRJ0r4nVkc96Rq1xd4mT71Dbk1kQHc8urB8A7aw==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.12.tgz", + "integrity": "sha512-MD0ElviEfAJY8qMOd6/jjSSvtqER2RDAi0lxe6EtUacC1DHCYkaPrKW4vLqY+tmZBg1yf+6n+uS77pXcHHcA3w==", "dependencies": { "tslib": "^2.3.0" }, @@ -453,13 +453,13 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.10" + "@angular/core": "16.2.12" } }, "node_modules/@angular/cdk": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.9.tgz", - "integrity": "sha512-TrLV68YpddUx3t2rs8W29CPk8YkgNGA8PKHwjB4Xvo1yaEH5XUnsw3MQCh42Ee7FKseaqzFgG85USZXAK0IB0A==", + "version": "16.2.13", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.13.tgz", + "integrity": "sha512-8kn2X2yesvgfIbCUNoS9EDjooIx9LwEglYBbD89Y/do8EeN/CC3Tn02gqSrEfgMhYBLBJmHXbfOhbDDvcvOCeg==", "dependencies": { "tslib": "^2.3.0" }, @@ -581,9 +581,9 @@ "dev": true }, "node_modules/@angular/common": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.10.tgz", - "integrity": "sha512-cLth66aboInNcWFjDBRmK30jC5KN10nKDDcv4U/r3TDTBpKOtnmTjNFFr7dmjfUmVhHFy/66piBMfpjZI93Rxg==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.12.tgz", + "integrity": "sha512-B+WY/cT2VgEaz9HfJitBmgdk4I333XG/ybC98CMC4Wz8E49T8yzivmmxXB3OD6qvjcOB6ftuicl6WBqLbZNg2w==", "dependencies": { "tslib": "^2.3.0" }, @@ -591,14 +591,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.10", + "@angular/core": "16.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.10.tgz", - "integrity": "sha512-ty6SfqkZlV2bLU/SSi3wmxrEFgPrK+WVslCNIr3FlTnCBdqpIbadHN2QB3A1d9XaNc7c4Tq5DQKh34cwMwNbuw==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.12.tgz", + "integrity": "sha512-6SMXUgSVekGM7R6l1Z9rCtUGtlg58GFmgbpMCsGf+VXxP468Njw8rjT2YZkf5aEPxEuRpSHhDYjqz7n14cwCXQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -606,7 +606,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.10" + "@angular/core": "16.2.12" }, "peerDependenciesMeta": { "@angular/core": { @@ -615,9 +615,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.10.tgz", - "integrity": "sha512-swgmtm4R23vQV9nJTXdDEFpOyIw3kz80mdT9qo3VId/2rqenOK253JsFypoqEj/fKzjV9gwXtTbmrMlhVyuyxw==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.12.tgz", + "integrity": "sha512-pWSrr152562ujh6lsFZR8NfNc5Ljj+zSTQO44DsuB0tZjwEpnRcjJEgzuhGXr+CoiBf+jTSPZKemtSktDk5aaA==", "dev": true, "dependencies": { "@babel/core": "7.23.2", @@ -638,7 +638,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.2.10", + "@angular/compiler": "16.2.12", "typescript": ">=4.9.3 <5.2" } }, @@ -688,12 +688,12 @@ } }, "node_modules/@angular/compiler-cli/node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.23.0", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -717,9 +717,9 @@ } }, "node_modules/@angular/core": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.10.tgz", - "integrity": "sha512-0XTsPjNflFhOl2CfNEdGeDOklG2t+m/D3g10Y7hg9dBjC1dURUEqTmM4d6J7JNbBURrP+/iP7uLsn3WRSipGUw==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.12.tgz", + "integrity": "sha512-GLLlDeke/NjroaLYOks0uyzFVo6HyLl7VOm0K1QpLXnYvW63W9Ql/T3yguRZa7tRkOAeFZ3jw+1wnBD4O8MoUA==", "dependencies": { "tslib": "^2.3.0" }, @@ -732,9 +732,9 @@ } }, "node_modules/@angular/forms": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.10.tgz", - "integrity": "sha512-TZliEtSWIL1UzY8kjed4QcMawWS8gk/H60KVgzCh83NGE0wd1OGv20Z5OR7O8j07dxB9vaxY7CQz/8eCz5KaNQ==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.12.tgz", + "integrity": "sha512-1Eao89hlBgLR3v8tU91vccn21BBKL06WWxl7zLpQmG6Hun+2jrThgOE4Pf3os4fkkbH4Apj0tWL2fNIWe/blbw==", "dependencies": { "tslib": "^2.3.0" }, @@ -742,16 +742,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.10", - "@angular/core": "16.2.10", - "@angular/platform-browser": "16.2.10", + "@angular/common": "16.2.12", + "@angular/core": "16.2.12", + "@angular/platform-browser": "16.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/material": { - "version": "16.2.9", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.2.9.tgz", - "integrity": "sha512-ppEVvB5+TAqYxEiWCOt56TJbKayuJXPO5gAIaoIgaj7a77A3iuJRBZD/TLldqUxqCI6T5pwuTVzdeDU4tTHGug==", + "version": "16.2.13", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.2.13.tgz", + "integrity": "sha512-7gP9KlaVZGpCeJlwXlD7puTiafAHYAbgYoODEQLbPCiAL/woFFDvM+DCdos7lmCBMyt6+10bkrPvz8cVfyTfQg==", "dependencies": { "@material/animation": "15.0.0-canary.bc9ae6c9c.0", "@material/auto-init": "15.0.0-canary.bc9ae6c9c.0", @@ -804,7 +804,7 @@ }, "peerDependencies": { "@angular/animations": "^16.0.0 || ^17.0.0", - "@angular/cdk": "16.2.9", + "@angular/cdk": "16.2.13", "@angular/common": "^16.0.0 || ^17.0.0", "@angular/core": "^16.0.0 || ^17.0.0", "@angular/forms": "^16.0.0 || ^17.0.0", @@ -813,9 +813,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.10.tgz", - "integrity": "sha512-TOZiK7ji550F8G39Ri255NnK1+2Xlr74RiElJdQct4TzfN0lqNf2KRDFFNwDohkP/78FUzcP4qBxs+Nf8M7OuQ==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.12.tgz", + "integrity": "sha512-NnH7ju1iirmVEsUq432DTm0nZBGQsBrU40M3ZeVHMQ2subnGiyUs3QyzDz8+VWLL/T5xTxWLt9BkDn65vgzlIQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -823,9 +823,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/animations": "16.2.10", - "@angular/common": "16.2.10", - "@angular/core": "16.2.10" + "@angular/animations": "16.2.12", + "@angular/common": "16.2.12", + "@angular/core": "16.2.12" }, "peerDependenciesMeta": { "@angular/animations": { @@ -834,9 +834,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.10.tgz", - "integrity": "sha512-YVmhAjOmsp2SWRonv6Mr/qXuKroCiew9asd1IlAZ//wqcml9ZrNAcX3WlDa8ZqdmOplQb0LuvvirfNB/6Is/jg==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.12.tgz", + "integrity": "sha512-ya54jerNgreCVAR278wZavwjrUWImMr2F8yM5n9HBvsMBbFaAQ83anwbOEiHEF2BlR+gJiEBLfpuPRMw20pHqw==", "dependencies": { "tslib": "^2.3.0" }, @@ -844,16 +844,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.10", - "@angular/compiler": "16.2.10", - "@angular/core": "16.2.10", - "@angular/platform-browser": "16.2.10" + "@angular/common": "16.2.12", + "@angular/compiler": "16.2.12", + "@angular/core": "16.2.12", + "@angular/platform-browser": "16.2.12" } }, "node_modules/@angular/router": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.10.tgz", - "integrity": "sha512-ndiq2NkGZ8hTsyL/KK8qsiR3UA0NjOFIn1jtGXOKtHryXZ6vSTtkhtkE4h4+G6/QNTL1IKtocFhOQt/xsc7DUA==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.12.tgz", + "integrity": "sha512-aU6QnYSza005V9P3W6PpkieL56O0IHps96DjqI1RS8yOJUl3THmokqYN4Fm5+HXy4f390FN9i6ftadYQDKeWmA==", "dependencies": { "tslib": "^2.3.0" }, @@ -861,9 +861,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.10", - "@angular/core": "16.2.10", - "@angular/platform-browser": "16.2.10", + "@angular/common": "16.2.12", + "@angular/core": "16.2.12", + "@angular/platform-browser": "16.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -1255,9 +1255,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -2667,12 +2667,12 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, @@ -2698,246 +2698,6 @@ "node": ">=10.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.17.tgz", - "integrity": "sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.17.tgz", - "integrity": "sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.17.tgz", - "integrity": "sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.17.tgz", - "integrity": "sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.17.tgz", - "integrity": "sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.17.tgz", - "integrity": "sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.17.tgz", - "integrity": "sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.17.tgz", - "integrity": "sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.17.tgz", - "integrity": "sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.17.tgz", - "integrity": "sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.17.tgz", - "integrity": "sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.17.tgz", - "integrity": "sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.17.tgz", - "integrity": "sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.17.tgz", - "integrity": "sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.17.tgz", - "integrity": "sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.18.17", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz", @@ -2954,102 +2714,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.17.tgz", - "integrity": "sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.17.tgz", - "integrity": "sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.17.tgz", - "integrity": "sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.17.tgz", - "integrity": "sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.17.tgz", - "integrity": "sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.17.tgz", - "integrity": "sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "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", @@ -3075,9 +2739,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -3120,9 +2784,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -3165,9 +2829,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", - "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4156,9 +3820,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.6.tgz", - "integrity": "sha512-d8ZlZL6dOtWmHdjG9PTGBkdiJMcsXD2tp6WeFRVvTEuvCI3XvKsUXBvJDE+mZOhzn5pUEYt+1TR5DHjDZbME3w==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.11.tgz", + "integrity": "sha512-4ndXJ4s94ZsryVGSDk/waIDrUqXqdGWftoOEn81Zu+nkL9ncI/G1fNUlSJ5OqeKmMLxMFouoy+BuJfvT+gEgnQ==", "dev": true, "engines": { "node": "^16.14.0 || >=18.10.0", @@ -4414,174 +4078,46 @@ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "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": "16.5.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.5.1.tgz", - "integrity": "sha512-q98TFI4B/9N9PmKUr1jcbtD4yAFs1HfYd9jUXXTQOlfO9SbDjnrYJgZ4Fp9rMNfrBhgIQ4x1qx0AukZccKmH9Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-darwin-x64": { - "version": "16.5.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-16.5.1.tgz", - "integrity": "sha512-j9HmL1l8k7EVJ3eOM5y8COF93gqrydpxCDoz23ZEtsY+JHY77VAiRQsmqBgEx9GGA2dXi9VEdS67B0+1vKariw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-freebsd-x64": { - "version": "16.5.1", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-16.5.1.tgz", - "integrity": "sha512-CXSPT01aVS869tvCCF2tZ7LnCa8l41wJ3mTVtWBkjmRde68E5Up093hklRMyXb3kfiDYlfIKWGwrV4r0eH6x1A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "16.5.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-16.5.1.tgz", - "integrity": "sha512-BhrumqJSZCWFfLFUKl4CAUwR0Y0G2H5EfFVGKivVecEQbb+INAek1aa6c89evg2/OvetQYsJ+51QknskwqvLsA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "16.5.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-16.5.1.tgz", - "integrity": "sha512-x7MsSG0W+X43WVv7JhiSq2eKvH2suNKdlUHEG09Yt0vm3z0bhtym1UCMUg3IUAK7jy9hhLeDaFVFkC6zo+H/XQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-arm64-musl": { - "version": "16.5.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-16.5.1.tgz", - "integrity": "sha512-J+/v/mFjOm74I0PNtH5Ka+fDd+/dWbKhpcZ2R1/6b9agzZk+Ff/SrwJcSYFXXWKbPX+uQ4RcJoytT06Zs3s0ow==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-x64-gnu": { - "version": "16.5.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.5.1.tgz", - "integrity": "sha512-igooWJ5YxQ94Zft7IqgL+Lw0qHaY15Btw4gfK756g/YTYLZEt4tTvR1y6RnK/wdpE3sa68bFTLVBNCGTyiTiDQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-x64-musl": { - "version": "16.5.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-16.5.1.tgz", - "integrity": "sha512-zF/exnPqFYbrLAduGhTmZ7zNEyADid2bzNQiIjJkh8Y6NpDwrQIwVIyvIxqynsjMrIs51kBH+8TUjKjj2Jgf5A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "rimraf": "^3.0.0" + }, "engines": { - "node": ">= 10" + "node": ">=8.17.0" } }, - "node_modules/@nx/nx-win32-arm64-msvc": { + "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-linux-x64-gnu": { "version": "16.5.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-16.5.1.tgz", - "integrity": "sha512-qtqiLS9Y9TYyAbbpq58kRoOroko4ZXg5oWVqIWFHoxc5bGPweQSJCROEqd1AOl2ZDC6BxfuVHfhDDop1kK05WA==", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.5.1.tgz", + "integrity": "sha512-igooWJ5YxQ94Zft7IqgL+Lw0qHaY15Btw4gfK756g/YTYLZEt4tTvR1y6RnK/wdpE3sa68bFTLVBNCGTyiTiDQ==", "cpu": [ - "arm64" + "x64" ], "dev": true, "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">= 10" } }, - "node_modules/@nx/nx-win32-x64-msvc": { + "node_modules/@nx/nx-linux-x64-musl": { "version": "16.5.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-16.5.1.tgz", - "integrity": "sha512-kUJBLakK7iyA9WfsGGQBVennA4jwf5XIgm0lu35oMOphtZIluvzItMt0EYBmylEROpmpEIhHq0P6J9FA+WH0Rg==", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-16.5.1.tgz", + "integrity": "sha512-zF/exnPqFYbrLAduGhTmZ7zNEyADid2bzNQiIjJkh8Y6NpDwrQIwVIyvIxqynsjMrIs51kBH+8TUjKjj2Jgf5A==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">= 10" @@ -4615,19 +4151,11 @@ "node": ">=14" } }, - "node_modules/@pkgr/utils": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", - "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "node_modules/@pkgr/core": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz", + "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==", "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "fast-glob": "^3.3.0", - "is-glob": "^4.0.3", - "open": "^9.1.0", - "picocolors": "^1.0.0", - "tslib": "^2.6.0" - }, "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, @@ -4635,36 +4163,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@pkgr/utils/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, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@pkgr/utils/node_modules/open": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", - "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", - "dev": true, - "dependencies": { - "default-browser": "^4.0.0", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@schematics/angular": { "version": "16.1.8", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.8.tgz", @@ -4823,9 +4321,9 @@ } }, "node_modules/@types/body-parser": { - "version": "1.19.4", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.4.tgz", - "integrity": "sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==", + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, "dependencies": { "@types/connect": "*", @@ -4833,27 +4331,27 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.12.tgz", - "integrity": "sha512-ky0kWSqXVxSqgqJvPIkgFkcn4C8MnRog308Ou8xBBIVo39OmUFy+jqNe0nPwLCDFxUpmT9EvT91YzOJgkDRcFg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect": { - "version": "3.4.37", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.37.tgz", - "integrity": "sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==", + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.2.tgz", - "integrity": "sha512-gX2j9x+NzSh4zOhnRPSdPPmTepS4DfxES0AvIFv3jGv5QyeAJf6u6dY5/BAoAJU9Qq1uTvwOku8SSC2GnCRl6Q==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, "dependencies": { "@types/express-serve-static-core": "*", @@ -4902,9 +4400,9 @@ "dev": true }, "node_modules/@types/express": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.20.tgz", - "integrity": "sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -4914,9 +4412,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.38", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.38.tgz", - "integrity": "sha512-hXOtc0tuDHZPFwwhuBJXPbjemWtXnJjbvuuyNH2Y5Z6in+iXc63c4eXYDc7GGGqHy+iwYqAJMdaItqdnbcBKmg==", + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", "dev": true, "dependencies": { "@types/node": "*", @@ -4926,15 +4424,15 @@ } }, "node_modules/@types/http-errors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.3.tgz", - "integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==", + "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 }, "node_modules/@types/http-proxy": { - "version": "1.17.13", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.13.tgz", - "integrity": "sha512-GkhdWcMNiR5QSQRYnJ+/oXzu0+7JJEPC8vkWXK351BkhjraZF+1W13CUYARUvX9+NqIU2n6YHA4iwywsc/M6Sw==", + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", "dev": true, "dependencies": { "@types/node": "*" @@ -4953,9 +4451,9 @@ "dev": true }, "node_modules/@types/mime": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.4.tgz", - "integrity": "sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, "node_modules/@types/node": { @@ -4967,16 +4465,25 @@ "undici-types": "~5.25.1" } }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { - "version": "6.9.9", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz", - "integrity": "sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==", + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", "dev": true }, "node_modules/@types/range-parser": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.6.tgz", - "integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==", + "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 }, "node_modules/@types/retry": { @@ -4992,9 +4499,9 @@ "dev": true }, "node_modules/@types/send": { - "version": "0.17.3", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.3.tgz", - "integrity": "sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==", + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, "dependencies": { "@types/mime": "^1", @@ -5002,18 +4509,18 @@ } }, "node_modules/@types/serve-index": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.3.tgz", - "integrity": "sha512-4KG+yMEuvDPRrYq5fyVm/I2uqAJSAwZK9VSa+Zf+zUq9/oxSSvy3kkIqyL+jjStv6UCVi8/Aho0NHtB1Fwosrg==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.4.tgz", - "integrity": "sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", "dev": true, "dependencies": { "@types/http-errors": "*", @@ -5022,18 +4529,18 @@ } }, "node_modules/@types/sockjs": { - "version": "0.3.35", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.35.tgz", - "integrity": "sha512-tIF57KB+ZvOBpAQwSaACfEu7htponHXaFzP7RfKYgsOS0NoYnn+9+jzp7bbq4fWerizI3dTB4NfAZoyeQKWJLw==", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/ws": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz", - "integrity": "sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dev": true, "dependencies": { "@types/node": "*" @@ -5865,9 +5372,9 @@ } }, "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true }, "node_modules/array-union": { @@ -5925,12 +5432,12 @@ } }, "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dev": true, "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -6080,15 +5587,6 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, - "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -6158,13 +5656,11 @@ "dev": true }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dev": true, "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } @@ -6175,18 +5671,6 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, - "node_modules/bplist-parser": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.44" - }, - "engines": { - "node": ">= 5.10.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6286,21 +5770,6 @@ "semver": "^7.0.0" } }, - "node_modules/bundle-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", - "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", - "dev": true, - "dependencies": { - "run-applescript": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "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", @@ -7194,150 +6663,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/default-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", - "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", - "dev": true, - "dependencies": { - "bundle-name": "^3.0.0", - "default-browser-id": "^3.0.0", - "execa": "^7.1.1", - "titleize": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", - "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", - "dev": true, - "dependencies": { - "bplist-parser": "^0.2.0", - "untildify": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/default-browser/node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/default-browser/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "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", @@ -7444,12 +6769,6 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -7908,15 +7227,15 @@ } }, "node_modules/eslint": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", - "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.52.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -7963,9 +7282,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -7975,23 +7294,24 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", - "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", + "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==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.5" + "synckit": "^0.8.6" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/prettier" + "url": "https://opencollective.com/eslint-plugin-prettier" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", + "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -8460,12 +7780,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/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 - }, "node_modules/express/node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -8810,9 +8124,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "dev": true, "funding": [ { @@ -9837,39 +9151,6 @@ "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, - "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-inside-container/node_modules/is-docker": { - "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, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "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", @@ -11560,9 +10841,9 @@ "dev": true }, "node_modules/ngx-mask": { - "version": "16.3.9", - "resolved": "https://registry.npmjs.org/ngx-mask/-/ngx-mask-16.3.9.tgz", - "integrity": "sha512-cptsvlI4OLI8Tpj23ZgSQDKz5jksyWGzAuEEn5pd58cq2oFGeeHZS2i1SQQi8kp+a+Dh/2RvDsfFmDWmI5Ln9w==", + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/ngx-mask/-/ngx-mask-16.4.2.tgz", + "integrity": "sha512-mQjcsTpctGu6HYKLf6/gjEUvW65D+46xvPIMYz0BDZXqHXrqKVluHXR3KF++TNOfdLLXwW6SvuHWd91NZN/C1A==", "dependencies": { "tslib": "^2.3.0" }, @@ -12909,9 +12190,9 @@ } }, "node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -12924,9 +12205,9 @@ } }, "node_modules/prettier-eslint": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.1.1.tgz", - "integrity": "sha512-SbtugbH80njB9QOPqb8C+W40Rvhr6iD0wrJTxk1Zx10rkY7KdjtSwHpf/WfiI3REboaXbvIOJXGiua3maIt0Sw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.2.0.tgz", + "integrity": "sha512-GDTSKc62VaLceiaI/qMaKo2oco2CIWtbj4Zr6ckhbTgcBL/uR0d9jkMzh9OtBIT/Z7iBoCB4OHj/aJ5YuNgAuA==", "dev": true, "dependencies": { "@typescript-eslint/parser": "^6.7.5", @@ -13432,9 +12713,9 @@ } }, "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", "dev": true }, "node_modules/regenerate": { @@ -13678,21 +12959,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-applescript": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", - "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", - "dev": true, - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -13863,11 +13129,12 @@ "dev": true }, "node_modules/selfsigned": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "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": { @@ -14613,13 +13880,13 @@ "dev": true }, "node_modules/synckit": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", - "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", "dev": true, "dependencies": { - "@pkgr/utils": "^2.3.1", - "tslib": "^2.5.0" + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -14845,18 +14112,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "node_modules/titleize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", - "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -15177,15 +14432,6 @@ "node": ">= 0.8" } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -15297,14 +14543,14 @@ } }, "node_modules/vite": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz", - "integrity": "sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", + "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", "dev": true, "dependencies": { "esbuild": "^0.18.10", - "postcss": "^8.4.26", - "rollup": "^3.25.2" + "postcss": "^8.4.27", + "rollup": "^3.27.1" }, "bin": { "vite": "bin/vite.js" @@ -15620,9 +14866,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/modules/ui/package.json b/modules/ui/package.json index c7ce21ec7..46f775272 100644 --- a/modules/ui/package.json +++ b/modules/ui/package.json @@ -17,44 +17,44 @@ }, "private": true, "dependencies": { - "@angular/animations": "^16.2.10", - "@angular/cdk": "^16.2.9", - "@angular/common": "^16.2.10", - "@angular/compiler": "^16.2.10", - "@angular/core": "^16.2.10", - "@angular/forms": "^16.2.10", - "@angular/material": "^16.2.9", - "@angular/platform-browser": "^16.2.10", - "@angular/platform-browser-dynamic": "^16.2.10", - "@angular/router": "^16.2.10", - "ngx-mask": "^16.3.9", + "@angular/animations": "^16.2.12", + "@angular/cdk": "^16.2.13", + "@angular/common": "^16.2.12", + "@angular/compiler": "^16.2.12", + "@angular/core": "^16.2.12", + "@angular/forms": "^16.2.12", + "@angular/material": "^16.2.13", + "@angular/platform-browser": "^16.2.12", + "@angular/platform-browser-dynamic": "^16.2.12", + "@angular/router": "^16.2.12", + "ngx-mask": "^16.4.2", "rxjs": "~7.8.0", "tslib": "^2.6.2", "zone.js": "~0.13.3" }, "devDependencies": { - "@angular-devkit/build-angular": "^16.2.6", + "@angular-devkit/build-angular": "^16.2.11", "@angular-eslint/builder": "16.2.0", "@angular-eslint/eslint-plugin": "16.2.0", "@angular-eslint/eslint-plugin-template": "16.2.0", "@angular-eslint/schematics": "16.2.0", "@angular-eslint/template-parser": "16.2.0", "@angular/cli": "~16.1.8", - "@angular/compiler-cli": "^16.2.10", + "@angular/compiler-cli": "^16.2.12", "@types/jasmine": "~4.3.6", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", - "eslint": "^8.49.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.1", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "jasmine-core": "~4.6.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", - "prettier": "^3.0.3", - "prettier-eslint": "^16.1.1", + "prettier": "^3.1.1", + "prettier-eslint": "^16.2.0", "typescript": "~5.1.3" } } diff --git a/modules/ui/src/app/app-routing.module.ts b/modules/ui/src/app/app-routing.module.ts index 59be3e59d..f9c349b7f 100644 --- a/modules/ui/src/app/app-routing.module.ts +++ b/modules/ui/src/app/app-routing.module.ts @@ -21,6 +21,7 @@ const routes: Routes = [ path: 'testrun', loadChildren: () => import('./progress/progress.module').then(m => m.ProgressModule), + title: 'Testrun', }, { path: 'devices', @@ -28,11 +29,13 @@ const routes: Routes = [ import('./device-repository/device-repository.module').then( m => m.DeviceRepositoryModule ), + title: 'Testrun - Devices', }, { path: 'reports', loadChildren: () => import('./history/history.module').then(m => m.HistoryModule), + title: 'Testrun - Reports', }, { path: '', diff --git a/modules/ui/src/app/app.component.html b/modules/ui/src/app/app.component.html index 9b78ec850..e6e351a99 100644 --- a/modules/ui/src/app/app.component.html +++ b/modules/ui/src/app/app.component.html @@ -64,7 +64,10 @@ (keydown.tab)="skipToNavigation($event)"> menu - +

Testrun

@@ -74,7 +77,7 @@

Testrun

class="app-toolbar-button app-toolbar-button-general-settings" mat-icon-button aria-label="Connection settings" - (click)="openGeneralSettings()"> + (click)="openGeneralSettings(true)"> tune @@ -84,8 +87,17 @@

Testrun

*ngIf=" (hasConnectionSetting$ | async) !== true && isConnectionSettingsLoaded "> - Step 1: To perform a device test, please, select ports in Connection - settings. + Step 1: To perform a device test, please, select ports in + Connection settings. Testrun (click)="navigateToDeviceRepository()" (keydown.enter)="navigateToDeviceRepository()" (keydown.space)="navigateToDeviceRepository()" + aria-label="The Create a Device link redirects to the Devices page and opens the dialogue there." tabindex="0" - role="button" + role="link" class="message-link" >Create a Device @@ -121,8 +134,9 @@

Testrun

(click)="navigateToRuntime()" (keydown.enter)="navigateToRuntime()" (keydown.space)="navigateToRuntime()" + aria-label="The Testrun link redirects to the Testrun page and opens the dialogue there." tabindex="0" - role="button" + role="link" class="message-link" >Testrun @@ -139,7 +153,6 @@

Testrun

diff --git a/modules/ui/src/app/app.component.scss b/modules/ui/src/app/app.component.scss index 6060205f1..9eb33e232 100644 --- a/modules/ui/src/app/app.component.scss +++ b/modules/ui/src/app/app.component.scss @@ -74,10 +74,6 @@ $nav-open-btn-width: 210px; line-height: 20px; letter-spacing: 0.25px; } - - app-version { - padding: 0 16px; - } } .app-sidebar { @@ -132,7 +128,7 @@ $nav-open-btn-width: 210px; .app-sidebar-button > .mat-icon { margin: 0 11px; min-width: 24px; - line-height: 18px; + line-height: 18px !important; } .app-sidebar-button-active { @@ -159,6 +155,7 @@ $nav-open-btn-width: 210px; .logo-link .mat-icon { width: 36px; height: 23px; + line-height: 18px !important; } .main-heading { @@ -206,4 +203,7 @@ app-version { margin-top: auto; margin-bottom: 16px; max-width: 100%; + width: $nav-close-width; + display: flex; + justify-content: center; } diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index 68bcc31c5..093ad3f35 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -38,7 +38,6 @@ import { AppRoutingModule } from './app-routing.module'; import { of } from 'rxjs/internal/observable/of'; import SpyObj = jasmine.SpyObj; import { BypassComponent } from './components/bypass/bypass.component'; -import { VersionComponent } from './components/version/version.component'; import { CalloutComponent } from './components/callout/callout.component'; import { MOCK_PROGRESS_DATA_IDLE, @@ -46,6 +45,7 @@ import { } from './mocks/progress.mock'; import { LoaderService } from './services/loader.service'; import { Routes } from './model/routes'; +import { StateService } from './services/state.service'; describe('AppComponent', () => { let component: AppComponent; @@ -54,6 +54,7 @@ describe('AppComponent', () => { let router: Router; let mockService: SpyObj; let mockLoaderService: SpyObj; + let mockStateService: SpyObj; const enterKeyEvent = new KeyboardEvent('keydown', { key: 'Enter', @@ -75,19 +76,22 @@ describe('AppComponent', () => { 'getSystemStatus', 'fetchHistory', 'getSystemInterfaces', - 'getVersion', - 'fetchVersion', 'setIsOpenAddDevice', 'systemStatus$', 'isTestrunStarted$', 'hasConnectionSetting$', + 'setIsOpenStartTestrun', ]); mockLoaderService = jasmine.createSpyObj(['setLoading']); + mockStateService = jasmine.createSpyObj('mockStateService', [ + 'focusFirstElementInMain', + ]); + mockService.getDevices.and.returnValue( new BehaviorSubject([device]) ); - mockService.getSystemInterfaces.and.returnValue(of([])); + mockService.getSystemInterfaces.and.returnValue(of({})); (mockService.systemStatus$ as unknown) = of({}); mockService.isTestrunStarted$ = of(true); mockService.hasConnectionSetting$ = of(true); @@ -103,17 +107,18 @@ describe('AppComponent', () => { MatToolbarModule, MatSidenavModule, BypassComponent, - VersionComponent, CalloutComponent, ], providers: [ { provide: TestRunService, useValue: mockService }, { provide: LoaderService, useValue: mockLoaderService }, + { provide: StateService, useValue: mockStateService }, ], declarations: [ AppComponent, FakeGeneralSettingsComponent, FakeSpinnerComponent, + FakeVersionComponent, ], }); @@ -228,6 +233,21 @@ describe('AppComponent', () => { }); })); + it('should call focusFirstElementInMain if settingsDrawer opened not from toggleBtn', fakeAsync(() => { + spyOn(component.settingsDrawer, 'close').and.returnValue( + Promise.resolve('close') + ); + + component.openGeneralSettings(false); + tick(); + component.closeSetting(); + flush(); + + component.settingsDrawer.close().then(() => { + expect(mockStateService.focusFirstElementInMain).toHaveBeenCalled(); + }); + })); + it('should call settingsDrawer open on openSetting', fakeAsync(() => { spyOn(component.settingsDrawer, 'open'); @@ -324,177 +344,220 @@ describe('AppComponent', () => { expect(version).toBeTruthy(); }); - describe('with no connection settings', () => { - beforeEach(() => { - mockService.hasConnectionSetting$ = of(false); - component.ngOnInit(); - fixture.detectChanges(); - }); + describe('Callout component visibility', () => { + describe('with no connection settings', () => { + beforeEach(() => { + mockService.hasConnectionSetting$ = of(false); + component.ngOnInit(); + fixture.detectChanges(); + }); - it('should have callout component with "Step 1" text', () => { - const callout = compiled.querySelector('app-callout'); - const calloutContent = callout?.innerHTML.trim(); + 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'); - }); - }); + expect(callout).toBeTruthy(); + expect(calloutContent).toContain('Step 1'); + }); - describe('with system status as "Idle"', () => { - beforeEach(() => { - mockService.hasConnectionSetting$ = of(true); - mockService.getDevices.and.returnValue( - new BehaviorSubject([device]) - ); - mockService.systemStatus$ = of(MOCK_PROGRESS_DATA_IDLE); - mockService.isTestrunStarted$ = of(false); - component.ngOnInit(); - fixture.detectChanges(); - }); + it('should have callout content with "Connection settings" link ', () => { + const calloutLinkEl = compiled.querySelector( + '.message-link' + ) as HTMLAnchorElement; + const calloutLinkContent = calloutLinkEl.innerHTML.trim(); - it('should have callout component with "Step 3" text', () => { - const callout = compiled.querySelector('app-callout'); - const calloutContent = callout?.innerHTML.trim(); + expect(calloutLinkEl).toBeTruthy(); + expect(calloutLinkContent).toContain('Connection settings'); + }); - expect(callout).toBeTruthy(); - expect(calloutContent).toContain('Step 3'); - }); - }); + 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; - describe('with no devices setted', () => { - beforeEach(() => { - mockService.getDevices.and.returnValue( - new BehaviorSubject(null) - ); - component.ngOnInit(); - fixture.detectChanges(); - }); + calloutLinkEl.dispatchEvent(testCase.event); + flush(); - it('should have callout component', () => { - const callout = compiled.querySelector('app-callout'); + expect(spyOpenSetting).toHaveBeenCalled(); + })); + }); + }); - expect(callout).toBeTruthy(); + describe('with system status as "Idle"', () => { + beforeEach(() => { + mockService.hasConnectionSetting$ = of(true); + mockService.getDevices.and.returnValue( + new BehaviorSubject([device]) + ); + mockService.systemStatus$ = of(MOCK_PROGRESS_DATA_IDLE); + mockService.isTestrunStarted$ = of(false); + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should have callout component with "Step 3" text', () => { + const callout = compiled.querySelector('app-callout'); + const calloutContent = callout?.innerHTML.trim(); + + expect(callout).toBeTruthy(); + expect(calloutContent).toContain('Step 3'); + }); }); - it('should have callout component with "Step 2" text', () => { - const callout = compiled.querySelector('app-callout'); - const calloutContent = callout?.innerHTML.trim(); + describe('with no devices setted', () => { + beforeEach(() => { + mockService.getDevices.and.returnValue( + new BehaviorSubject(null) + ); + component.ngOnInit(); + fixture.detectChanges(); + }); - expect(callout).toBeTruthy(); - expect(calloutContent).toContain('Step 2'); - }); + it('should have callout component', () => { + const callout = compiled.querySelector('app-callout'); - it('should have callout content with "Create a Device" link ', () => { - const calloutLinkEl = compiled.querySelector( - '.message-link' - ) as HTMLAnchorElement; - const calloutLinkContent = calloutLinkEl.innerHTML.trim(); + expect(callout).toBeTruthy(); + }); - expect(calloutLinkEl).toBeTruthy(); - expect(calloutLinkContent).toContain('Create a Device'); - }); + it('should have callout component with "Step 2" text', () => { + const callout = compiled.querySelector('app-callout'); + const calloutContent = callout?.innerHTML.trim(); - keyboardCases.forEach(testCase => { - it(`should navigate to the device-repository on keydown ${testCase.name} "Create a Device" link`, fakeAsync(() => { + expect(callout).toBeTruthy(); + expect(calloutContent).toContain('Step 2'); + }); + + it('should have callout content with "Create a Device" link ', () => { const calloutLinkEl = compiled.querySelector( '.message-link' ) as HTMLAnchorElement; + const calloutLinkContent = calloutLinkEl.innerHTML.trim(); - calloutLinkEl.dispatchEvent(testCase.event); - flush(); - - expect(router.url).toBe(Routes.Devices); - })); - }); - - it('should navigate to the device-repository on click "Create a Device" link', fakeAsync(() => { - const calloutLinkEl = compiled.querySelector( - '.message-link' - ) as HTMLAnchorElement; + expect(calloutLinkEl).toBeTruthy(); + expect(calloutLinkContent).toContain('Create a Device'); + }); - calloutLinkEl.click(); - flush(); - - expect(router.url).toBe(Routes.Devices); - expect(mockService.setIsOpenAddDevice).toHaveBeenCalledWith(true); - })); - }); + 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' + ) as HTMLAnchorElement; - describe('with devices setted but without systemStatus data', () => { - beforeEach(() => { - mockService.getDevices.and.returnValue( - new BehaviorSubject([device]) - ); - mockService.isTestrunStarted$ = of(false); - component.ngOnInit(); - fixture.detectChanges(); - }); + calloutLinkEl.dispatchEvent(testCase.event); + flush(); - it('should have callout component with "Step 3" text', () => { - const callout = compiled.querySelector('app-callout'); - const calloutContent = callout?.innerHTML.trim(); + expect(router.url).toBe(Routes.Devices); + })); + }); - expect(callout).toBeTruthy(); - expect(calloutContent).toContain('Step 3'); - }); + it('should navigate to the device-repository on click "Create a Device" link', fakeAsync(() => { + const calloutLinkEl = compiled.querySelector( + '.message-link' + ) as HTMLAnchorElement; - it('should have callout component with "Testrun" link', () => { - const callout = compiled.querySelector('app-callout'); - const calloutLinkEl = compiled.querySelector( - '.message-link' - ) as HTMLAnchorElement; - const calloutLinkContent = calloutLinkEl.innerHTML.trim(); + calloutLinkEl.click(); + flush(); - expect(callout).toBeTruthy(); - expect(calloutLinkContent).toContain('Testrun'); + expect(router.url).toBe(Routes.Devices); + expect(mockService.setIsOpenAddDevice).toHaveBeenCalledWith(true); + })); }); - keyboardCases.forEach(testCase => { - it(`should navigate to the runtime on keydown ${testCase.name} "Run the Test" link`, fakeAsync(() => { + describe('with devices setted but without systemStatus data', () => { + beforeEach(() => { + mockService.getDevices.and.returnValue( + new BehaviorSubject([device]) + ); + mockService.isTestrunStarted$ = of(false); + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should have callout component with "Step 3" text', () => { + const callout = compiled.querySelector('app-callout'); + const calloutContent = callout?.innerHTML.trim(); + + expect(callout).toBeTruthy(); + expect(calloutContent).toContain('Step 3'); + }); + + it('should have callout component with "Testrun" link', () => { + const callout = compiled.querySelector('app-callout'); const calloutLinkEl = compiled.querySelector( '.message-link' ) as HTMLAnchorElement; + const calloutLinkContent = calloutLinkEl.innerHTML.trim(); - calloutLinkEl.dispatchEvent(testCase.event); - flush(); + expect(callout).toBeTruthy(); + expect(calloutLinkContent).toContain('Testrun'); + }); - expect(router.url).toBe(Routes.Testrun); - })); - }); - }); + keyboardCases.forEach(testCase => { + it(`should navigate to the runtime on keydown ${testCase.name} "Run the Test" link`, fakeAsync(() => { + const calloutLinkEl = compiled.querySelector( + '.message-link' + ) as HTMLAnchorElement; - describe('with devices setted, without systemStatus data, but run the tests ', () => { - beforeEach(() => { - mockService.getDevices.and.returnValue( - new BehaviorSubject([device]) - ); - mockService.isTestrunStarted$ = of(true); - component.ngOnInit(); - fixture.detectChanges(); + calloutLinkEl.dispatchEvent(testCase.event); + flush(); + + expect(router.url).toBe(Routes.Testrun); + })); + }); }); - it('should not have callout component', () => { - const callout = compiled.querySelector('app-callout'); + describe('with devices setted, without systemStatus data, but run the tests ', () => { + beforeEach(() => { + mockService.getDevices.and.returnValue( + new BehaviorSubject([device]) + ); + mockService.isTestrunStarted$ = of(true); + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should not have callout component', () => { + const callout = compiled.querySelector('app-callout'); + + expect(callout).toBeNull(); + }); + }); - expect(callout).toBeNull(); + describe('with devices setted and systemStatus data ', () => { + beforeEach(() => { + mockService.getDevices.and.returnValue( + new BehaviorSubject([device]) + ); + mockService.systemStatus$ = of(MOCK_PROGRESS_DATA_IN_PROGRESS); + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should not have callout component', () => { + const callout = compiled.querySelector('app-callout'); + + expect(callout).toBeNull(); + }); }); }); - describe('with devices setted and systemStatus data ', () => { - beforeEach(() => { - mockService.getDevices.and.returnValue( - new BehaviorSubject([device]) - ); - mockService.systemStatus$ = of(MOCK_PROGRESS_DATA_IN_PROGRESS); - component.ngOnInit(); - fixture.detectChanges(); - }); + it('should not call toggleSettingsBtn focus on closeSetting when device length is 0', async () => { + mockService.getDevices.and.returnValue( + new BehaviorSubject([]) + ); + component.ngOnInit(); + fixture.detectChanges(); - it('should not have callout component', () => { - const callout = compiled.querySelector('app-callout'); + spyOn(component.settingsDrawer, 'close').and.returnValue( + Promise.resolve('close') + ); + const spyToggle = spyOn(component.toggleSettingsBtn, 'focus'); - expect(callout).toBeNull(); - }); + await component.closeSetting(); + + expect(spyToggle).toHaveBeenCalledTimes(0); }); }); @@ -505,7 +568,6 @@ describe('AppComponent', () => { class FakeGeneralSettingsComponent { @Input() interfaces = []; @Output() closeSettingEvent = new EventEmitter(); - @Output() openSettingEvent = new EventEmitter(); @Output() reloadInterfacesEvent = new EventEmitter(); } @@ -514,3 +576,9 @@ class FakeGeneralSettingsComponent { template: '
', }) class FakeSpinnerComponent {} + +@Component({ + selector: 'app-version', + template: '
', +}) +class FakeVersionComponent {} diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index 41eb73ab2..287d8ba20 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -23,7 +23,7 @@ import { import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { MatDrawer } from '@angular/material/sidenav'; -import { TestRunService } from './services/test-run.service'; +import { SystemInterfaces, TestRunService } from './services/test-run.service'; import { Observable } from 'rxjs/internal/Observable'; import { Device } from './model/device'; import { take } from 'rxjs'; @@ -34,6 +34,7 @@ import { CalloutType } from './model/callout-type'; import { tap } from 'rxjs/internal/operators/tap'; import { shareReplay } from 'rxjs/internal/operators/shareReplay'; import { Routes } from './model/routes'; +import { StateService } from './services/state.service'; const DEVICES_LOGO_URL = '/assets/icons/devices.svg'; const REPORTS_LOGO_URL = '/assets/icons/reports.svg'; @@ -52,13 +53,14 @@ export class AppComponent implements OnInit { systemStatus$!: Observable; isTestrunStarted$!: Observable; hasConnectionSetting$!: Observable; - interfaces: string[] = []; + interfaces: SystemInterfaces = {}; isDevicesLoaded = false; isStatusLoaded = false; isConnectionSettingsLoaded = false; public readonly StatusOfTestrun = StatusOfTestrun; public readonly Routes = Routes; - + private devicesLength = 0; + private openedSettingFromToggleBtn = true; @ViewChild('settingsDrawer') public settingsDrawer!: MatDrawer; @ViewChild('toggleSettingsBtn') public toggleSettingsBtn!: HTMLButtonElement; @ViewChild('navigation') public navigation!: ElementRef; @@ -69,6 +71,7 @@ export class AppComponent implements OnInit { private domSanitizer: DomSanitizer, private testRunService: TestRunService, private readonly loaderService: LoaderService, + private readonly state: StateService, private route: Router ) { this.testRunService.fetchDevices(); @@ -99,7 +102,10 @@ export class AppComponent implements OnInit { this.devices$ = this.testRunService.getDevices().pipe( tap(result => { if (result !== null) { + this.devicesLength = result.length; this.isDevicesLoaded = true; + } else { + this.devicesLength = 0; } }), shareReplay({ refCount: true, bufferSize: 1 }) @@ -128,16 +134,23 @@ export class AppComponent implements OnInit { navigateToRuntime(): void { this.route.navigate([Routes.Testrun]); + this.testRunService.setIsOpenStartTestrun(true); } async closeSetting(): Promise { - return await this.settingsDrawer - .close() - .then(() => this.toggleSettingsBtn.focus()); + return await this.settingsDrawer.close().then(() => { + if (this.devicesLength > 0) { + this.toggleSettingsBtn.focus(); + } // else device create window will be opened + + if (!this.openedSettingFromToggleBtn) { + this.state.focusFirstElementInMain(); + } + }); } async openSetting(): Promise { - return await this.openGeneralSettings(); + return await this.openGeneralSettings(false); } reloadInterfaces(): void { @@ -169,7 +182,8 @@ export class AppComponent implements OnInit { } } - async openGeneralSettings() { + async openGeneralSettings(openSettingFromToggleBtn: boolean) { + this.openedSettingFromToggleBtn = openSettingFromToggleBtn; this.getSystemInterfaces(); await this.settingsDrawer.open(); } diff --git a/modules/ui/src/app/app.module.ts b/modules/ui/src/app/app.module.ts index 67d4c3464..e130fda29 100644 --- a/modules/ui/src/app/app.module.ts +++ b/modules/ui/src/app/app.module.ts @@ -30,6 +30,8 @@ import { GeneralSettingsComponent } from './components/general-settings/general- 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 { ErrorInterceptor } from './interceptors/error.interceptor'; import { LoadingInterceptor } from './interceptors/loading.interceptor'; import { SpinnerComponent } from './components/spinner/spinner.component'; @@ -49,6 +51,8 @@ import { CalloutComponent } from './components/callout/callout.component'; MatSidenavModule, MatButtonToggleModule, MatRadioModule, + MatInputModule, + MatSelectModule, HttpClientModule, ReactiveFormsModule, MatFormFieldModule, 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 ab8919049..93430f5c4 100644 --- a/modules/ui/src/app/components/bypass/bypass.component.spec.ts +++ b/modules/ui/src/app/components/bypass/bypass.component.spec.ts @@ -17,6 +17,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BypassComponent } from './bypass.component'; import { Component } from '@angular/core'; +import { StateService } from '../../services/state.service'; +import SpyObj = jasmine.SpyObj; @Component({ selector: 'app-test-bypass', @@ -29,11 +31,14 @@ class TestBypassComponent {} describe('BypassComponent', () => { let component: TestBypassComponent; let fixture: ComponentFixture; + let mockService: SpyObj; beforeEach(() => { + mockService = jasmine.createSpyObj(['focusFirstElementInMain']); TestBed.configureTestingModule({ imports: [BypassComponent], declarations: [TestBypassComponent], + providers: [{ provide: StateService, useValue: mockService }], }); fixture = TestBed.createComponent(TestBypassComponent); component = fixture.componentInstance; @@ -49,13 +54,10 @@ describe('BypassComponent', () => { const button = fixture.nativeElement.querySelector( '.navigation-bypass-button' ) as HTMLButtonElement; - const testButton = fixture.nativeElement.querySelector( - '#test-button' - ) as HTMLButtonElement; button?.click(); - expect(document.activeElement).toBe(testButton); + expect(mockService.focusFirstElementInMain).toHaveBeenCalled(); }); }); }); diff --git a/modules/ui/src/app/components/bypass/bypass.component.ts b/modules/ui/src/app/components/bypass/bypass.component.ts index 218a0b3c5..ad29f4544 100644 --- a/modules/ui/src/app/components/bypass/bypass.component.ts +++ b/modules/ui/src/app/components/bypass/bypass.component.ts @@ -16,6 +16,7 @@ import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-bypass', @@ -25,13 +26,9 @@ import { MatButtonModule } from '@angular/material/button'; styleUrls: ['./bypass.component.scss'], }) export class BypassComponent { + constructor(private readonly state: StateService) {} skipToMainContent(event: Event) { event.preventDefault(); - const firstControl: HTMLElement | null = window.document.querySelector( - '#main button:not(disabled), #main table' - ); - if (firstControl) { - firstControl.focus(); - } + this.state.focusFirstElementInMain(); } } diff --git a/modules/ui/src/app/components/callout/callout.component.scss b/modules/ui/src/app/components/callout/callout.component.scss index dc7d78e5d..d6b6f26c5 100644 --- a/modules/ui/src/app/components/callout/callout.component.scss +++ b/modules/ui/src/app/components/callout/callout.component.scss @@ -32,7 +32,7 @@ min-height: 48px; padding: 6px 24px; border-radius: 8px; - align-items: flex-start; + align-items: center; gap: 16px; } diff --git a/modules/ui/src/app/components/delete-form/delete-form.component.html b/modules/ui/src/app/components/delete-form/delete-form.component.html index cafeba26c..e63e204b8 100644 --- a/modules/ui/src/app/components/delete-form/delete-form.component.html +++ b/modules/ui/src/app/components/delete-form/delete-form.component.html @@ -19,10 +19,20 @@

- - diff --git a/modules/ui/src/app/components/delete-report/delete-report.component.spec.ts b/modules/ui/src/app/components/delete-report/delete-report.component.spec.ts index 766bf8b4e..11e9c2a2e 100644 --- a/modules/ui/src/app/components/delete-report/delete-report.component.spec.ts +++ b/modules/ui/src/app/components/delete-report/delete-report.component.spec.ts @@ -55,6 +55,7 @@ describe('DeleteReportComponent', () => { }); it('#deleteReport should open delete dialog', () => { + const deviceRemovedSpy = spyOn(component.deviceRemoved, 'emit'); spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), } as MatDialogRef); @@ -71,6 +72,7 @@ describe('DeleteReportComponent', () => { '01:02:03:04:05:06', '2023-06-22T09:20:00.123Z' ); + expect(deviceRemovedSpy).toHaveBeenCalled(); }); }); diff --git a/modules/ui/src/app/components/delete-report/delete-report.component.ts b/modules/ui/src/app/components/delete-report/delete-report.component.ts index 16d82ac52..4f76b1859 100644 --- a/modules/ui/src/app/components/delete-report/delete-report.component.ts +++ b/modules/ui/src/app/components/delete-report/delete-report.component.ts @@ -13,7 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + OnDestroy, + Output, +} from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { ReportActionComponent } from '../report-action/report-action.component'; @@ -36,6 +42,7 @@ export class DeleteReportComponent extends ReportActionComponent implements OnDestroy { + @Output() deviceRemoved = new EventEmitter(); private destroy$: Subject = new Subject(); constructor( private testRunService: TestRunService, @@ -72,6 +79,7 @@ export class DeleteReportComponent this.testRunService .deleteReport(this.data.device.mac_addr, this.data.started || '') .subscribe(() => { + this.deviceRemoved.emit(); this.testRunService.removeReport( this.data.device.mac_addr, this.data.started || '' 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 19683b6f1..7fc675c86 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 @@ -19,12 +19,12 @@ [attr.aria-label]="label" class="device-item" type="button"> -
- {{ device.model }} -
{{ device.manufacturer }}
+
+ {{ device.model }} +
{{ device.mac_addr }}
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 4e9e88b49..b7025ff76 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 @@ -32,17 +32,17 @@ $border-radius: 12px; grid-row-gap: 4px; font-family: 'Open Sans', sans-serif; grid-template-areas: - 'name name icon' - 'manufacturer address icon'; + 'manufacturer manufacturer icon' + 'name address icon'; &:hover { cursor: pointer; } } -.item-name { +.item-manufacturer { padding: 0 16px; - grid-area: name; + grid-area: manufacturer; justify-self: start; align-self: end; color: #1f1f1f; @@ -57,9 +57,9 @@ $border-radius: 12px; text-align: start; } -.item-manufacturer { +.item-name { padding: 0 16px; - grid-area: manufacturer; + grid-area: name; justify-self: start; color: $grey-800; font-size: 14px; 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 a72d2516d..dfb161089 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 @@ -32,6 +32,6 @@ export class DeviceItemComponent { } get label() { - return `${this.device.model} ${this.device.manufacturer} ${this.device.mac_addr}`; + return `${this.device.manufacturer} ${this.device.model} ${this.device.mac_addr}`; } } diff --git a/modules/ui/src/app/components/filter-chips/filter-chips.component.html b/modules/ui/src/app/components/filter-chips/filter-chips.component.html index 512862023..07cf43aa0 100644 --- a/modules/ui/src/app/components/filter-chips/filter-chips.component.html +++ b/modules/ui/src/app/components/filter-chips/filter-chips.component.html @@ -20,11 +20,16 @@ class="filter-chip" *ngIf="!isValueEmpty(item.value)" (removed)="clearFilter(item.key)"> - Device + Device contains "{{ item.value }}" - Firmware + Firmware contains "{{ item.value }}" + + + {{ item.value }} - {{ item.value }}
- - Warning! Testrun requires two ports to operate correctly. - - -

- Choose one of the following ports on your Laptop or Desktop -

- - - {{ interface }} - - - -

- Choose one of the options where you’ll connect IoT device for testing + [formGroup]="settingForm" + [class.setting-drawer-content-form-empty]=" + (interfaces | keyvalue).length === 0 + "> + + + Warning! Testrun requires two ports to operate correctly. + + +

+ Choose one of the options where you’ll connect IoT device for testing +

+ + Device connection port + + + +

+ {{ deviceControl.value.key }} +

+

+ {{ deviceControl.value.value }} +

+
+
+ +

{{ interface.key }}

+

{{ interface.value }}

+
+
+
+ +

+ Choose one of the following ports on your Laptop or Desktop +

+ + Internet connection port + + + +

+ {{ internetControl.value.key }} +

+

+ {{ internetControl.value.value }} +

+
+
+ +

{{ defaultInternetOption.key }}

+

{{ defaultInternetOption.value }}

+
+ +

{{ interface.key }}

+

{{ interface.value }}

+
+
+
+ +

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

- - - {{ interface }} - - + + Both interfaces must have different values + +
@@ -83,37 +160,4 @@

Connection settings

-

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

- - Both interfaces must have different values - - diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.scss b/modules/ui/src/app/components/general-settings/general-settings.component.scss index 84d49e514..01e13dcef 100644 --- a/modules/ui/src/app/components/general-settings/general-settings.component.scss +++ b/modules/ui/src/app/components/general-settings/general-settings.component.scss @@ -56,20 +56,25 @@ } .setting-drawer-content { - padding: 11px 16px 16px; + padding: 11px 16px 8px 16px; overflow: hidden; + flex: 1; form { display: grid; - grid-template-rows: repeat(4, auto); + grid-template-rows: repeat(7, auto) 1fr; height: 100%; } + + .setting-drawer-content-form-empty { + grid-template-rows: repeat(2, auto) 1fr; + } } .error-message-container { display: block; margin-top: auto; - padding: 0 16px; + padding-bottom: 8px; } .error-message-container + .setting-drawer-footer { @@ -79,11 +84,6 @@ .setting-form-label { font-size: 18px; color: $dark-grey; - - &.device-label { - display: inline-block; - padding-top: 16px; - } } :host:has(.two-ports-message) .internet-label { @@ -95,38 +95,56 @@ font-size: 14px; line-height: 20px; letter-spacing: 0.2px; - margin: 10px 0 0; + margin: 10px 0; color: $dark-grey; } -.setting-radio-group { - display: flex; - flex-direction: column; - margin-left: -10px; - align-items: flex-start; - overflow: scroll; +.setting-option-value { + padding: 14px 16px; } -.setting-radio-button { - padding: 8px 0; +.option-value { + margin: 0; + font-family: Roboto; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.2px; - ::ng-deep .mdc-form-field > label { + &.top { + color: #3c4043; + } + + &.bottom { + color: #5f6368; + } +} + +.setting-field { + ::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: 16px; + font-size: 14px; font-style: normal; font-weight: 400; - line-height: 24px; - letter-spacing: 0.1px; - color: $grey-800; - max-width: 240px; - overflow: hidden; - text-overflow: ellipsis; + line-height: 20px; + letter-spacing: 0.2px; + } + + ::ng-deep .mat-mdc-floating-label:not(.mdc-floating-label--float-above) { + top: 35px; } } .message { margin: 0; - padding: 0 16px 16px; + padding: 16px 0 0 0; color: $grey-800; font-family: $font-secondary; font-size: 14px; @@ -135,11 +153,11 @@ } .setting-drawer-footer { + padding: 0 8px; margin-top: auto; display: flex; flex-shrink: 0; justify-content: flex-end; - padding: 16px 24px 8px 24px; .close-button, .save-button { @@ -153,7 +171,7 @@ .close-button { margin-right: 10px; &:enabled { - color: $secondary; + color: $primary; } } } diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.spec.ts b/modules/ui/src/app/components/general-settings/general-settings.component.spec.ts index 6448b80a1..e9b82ddca 100644 --- a/modules/ui/src/app/components/general-settings/general-settings.component.spec.ts +++ b/modules/ui/src/app/components/general-settings/general-settings.component.spec.ts @@ -13,15 +13,13 @@ * 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, TestBed } from '@angular/core/testing'; import { GeneralSettingsComponent } from './general-settings.component'; -import { TestRunService } from '../../services/test-run.service'; +import { + SystemInterfaces, + TestRunService, +} from '../../services/test-run.service'; import { of } from 'rxjs'; import { SystemConfig } from '../../model/setting'; import { MatRadioModule } from '@angular/material/radio'; @@ -30,6 +28,11 @@ 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'; const MOCK_SYSTEM_CONFIG_EMPTY: SystemConfig = { network: { @@ -40,21 +43,21 @@ const MOCK_SYSTEM_CONFIG_EMPTY: SystemConfig = { const MOCK_SYSTEM_CONFIG_WITH_DATA: SystemConfig = { network: { - device_intf: 'mockDeviceValue', - internet_intf: 'mockInternetValue', + device_intf: 'mockDeviceKey', + internet_intf: 'mockInternetKey', }, }; -const MOCK_SYSTEM_CONFIG_WITH_ONE_SETTING: SystemConfig = { - network: { - device_intf: 'mockDeviceValue', - }, +const MOCK_INTERFACES: SystemInterfaces = { + mockDeviceKey: 'mockDeviceValue', + mockInternetKey: 'mockInternetValue', }; describe('GeneralSettingsComponent', () => { let component: GeneralSettingsComponent; let fixture: ComponentFixture; - let testRunServiceMock: jasmine.SpyObj; + let testRunServiceMock: SpyObj; + let mockLiveAnnouncer: SpyObj; let compiled: HTMLElement; beforeEach(async () => { @@ -63,19 +66,22 @@ describe('GeneralSettingsComponent', () => { 'getSystemConfig', 'setSystemConfig', 'createSystemConfig', - 'setIsOpenAddDevice', 'hasConnectionSetting$', 'setHasConnectionSetting', + 'systemConfig$', ]); - testRunServiceMock.getSystemInterfaces.and.returnValue(of([])); + testRunServiceMock.getSystemInterfaces.and.returnValue(of({})); testRunServiceMock.getSystemConfig.and.returnValue( of(MOCK_SYSTEM_CONFIG_EMPTY) ); testRunServiceMock.createSystemConfig.and.returnValue( of(MOCK_SYSTEM_CONFIG_WITH_DATA) ); + testRunServiceMock.systemConfig$ = of(MOCK_SYSTEM_CONFIG_EMPTY); testRunServiceMock.hasConnectionSetting$ = of(true); + mockLiveAnnouncer = jasmine.createSpyObj(['announce']); + await TestBed.configureTestingModule({ declarations: [ GeneralSettingsComponent, @@ -83,13 +89,19 @@ describe('GeneralSettingsComponent', () => { FakeSpinnerComponent, FakeCalloutComponent, ], - providers: [{ provide: TestRunService, useValue: testRunServiceMock }], + providers: [ + { provide: TestRunService, useValue: testRunServiceMock }, + { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, + ], imports: [ + BrowserAnimationsModule, MatButtonModule, MatIconModule, MatRadioModule, ReactiveFormsModule, MatIconTestingModule, + MatInputModule, + MatSelectModule, ], }).compileComponents(); @@ -107,14 +119,15 @@ describe('GeneralSettingsComponent', () => { testRunServiceMock.getSystemConfig.and.returnValue( of(MOCK_SYSTEM_CONFIG_WITH_DATA) ); + component.interfaces = { mockDeviceKey: 'mockDeviceValue' }; + + const expectedDevice = { key: 'mockDeviceKey', value: 'mockDeviceValue' }; component.ngOnInit(); - expect(component.deviceControl.value).toBe( - MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.device_intf - ); - expect(component.internetControl.value).toBe( - MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.internet_intf + expect(component.deviceControl.value).toEqual(expectedDevice); + expect(component.internetControl.value).toEqual( + component.defaultInternetOption ); }); @@ -126,76 +139,51 @@ describe('GeneralSettingsComponent', () => { expect(component.reloadInterfacesEvent.emit).toHaveBeenCalled(); }); - describe('#openSetting', () => { - it('should call openSetting if device and internet data are unavailable', () => { - spyOn(component.openSettingEvent, 'emit'); - - component.ngOnInit(); - - expect(component.openSettingEvent.emit).toHaveBeenCalled(); - }); - - it('should call openSetting if not systemConfig data', fakeAsync(() => { - spyOn(component.openSettingEvent, 'emit'); - testRunServiceMock.getSystemConfig.and.returnValue(of({})); - tick(); - - component.ngOnInit(); - - expect(component.openSettingEvent.emit).toHaveBeenCalled(); - })); - - it('should call openSetting if only one setting available', fakeAsync(() => { - spyOn(component.openSettingEvent, 'emit'); - testRunServiceMock.getSystemConfig.and.returnValue( - of(MOCK_SYSTEM_CONFIG_WITH_ONE_SETTING) - ); - tick(); - - component.ngOnInit(); - - expect(component.openSettingEvent.emit).toHaveBeenCalled(); - })); - - it('should not call openSetting if device and internet data are available', fakeAsync(() => { - spyOn(component.openSettingEvent, 'emit'); - testRunServiceMock.getSystemConfig.and.returnValue( - of(MOCK_SYSTEM_CONFIG_WITH_DATA) - ); - tick(); - - component.ngOnInit(); - - expect(component.openSettingEvent.emit).not.toHaveBeenCalled(); - })); - }); - describe('#closeSetting', () => { beforeEach(() => { testRunServiceMock.systemConfig$ = of(MOCK_SYSTEM_CONFIG_WITH_DATA); + component.interfaces = MOCK_INTERFACES; }); it('should emit closeSettingEvent', () => { spyOn(component.closeSettingEvent, 'emit'); - component.closeSetting(); + 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 connection setting panel is closed.` + ); + }); + it('should call reset settingForm', () => { spyOn(component.settingForm, 'reset'); - component.closeSetting(); + component.closeSetting('Message'); expect(component.settingForm.reset).toHaveBeenCalled(); }); it('should set value of settingForm on setSystemSetting', () => { - component.closeSetting(); + component.closeSetting('Message'); + + const expectedDevice = { key: 'mockDeviceKey', value: 'mockDeviceValue' }; + const expectedInternet = { + key: 'mockInternetKey', + value: 'mockInternetValue', + }; + + expect(component.settingForm.value.device_intf).toEqual(expectedDevice); - expect(component.settingForm.value).toEqual( - MOCK_SYSTEM_CONFIG_WITH_DATA.network + expect(component.settingForm.value.internet_intf).toEqual( + expectedInternet ); }); }); @@ -218,38 +206,31 @@ describe('GeneralSettingsComponent', () => { }); it('should call createSystemConfig when setting form valid', () => { - component.deviceControl.setValue( - MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.device_intf - ); - component.internetControl.setValue( - MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.internet_intf - ); + const expectedResult = { + network: { + device_intf: 'mockDeviceKey', + internet_intf: '', + }, + }; + + component.deviceControl.setValue({ + key: 'mockDeviceKey', + value: 'mockDeviceValue', + }); + component.internetControl.setValue(component.defaultInternetOption); component.saveSetting(); expect(component.settingForm.invalid).toBeFalse(); expect(testRunServiceMock.createSystemConfig).toHaveBeenCalledWith( - MOCK_SYSTEM_CONFIG_WITH_DATA - ); - }); - - it('should setIsOpenAddDevice as true on first save setting', () => { - component.deviceControl.setValue( - MOCK_SYSTEM_CONFIG_WITH_DATA.network?.device_intf - ); - component.internetControl.setValue( - MOCK_SYSTEM_CONFIG_WITH_DATA.network?.internet_intf + expectedResult ); - - component.saveSetting(); - - expect(testRunServiceMock.setIsOpenAddDevice).toHaveBeenCalledWith(true); }); }); describe('with no intefaces data', () => { beforeEach(() => { - component.interfaces = []; + component.interfaces = {}; fixture.detectChanges(); }); @@ -270,7 +251,7 @@ describe('GeneralSettingsComponent', () => { describe('with intefaces lenght less then two', () => { beforeEach(() => { - component.interfaces = ['mockDeviceValue']; + component.interfaces = { mockDeviceValue: 'mockDeviceValue' }; testRunServiceMock.systemConfig$ = of(MOCK_SYSTEM_CONFIG_WITH_DATA); testRunServiceMock.getSystemConfig.and.returnValue( of(MOCK_SYSTEM_CONFIG_WITH_DATA) @@ -301,9 +282,12 @@ describe('GeneralSettingsComponent', () => { }); }); - describe('with intefaces lenght more then one', () => { + describe('with interfaces length more then one', () => { beforeEach(() => { - component.interfaces = ['mockDeviceValue', 'mockInternetValue']; + component.interfaces = { + mockDeviceValue: 'mockDeviceValue', + mockInterfaceValue: 'mockInterfaceValue', + }; testRunServiceMock.systemConfig$ = of(MOCK_SYSTEM_CONFIG_WITH_DATA); testRunServiceMock.getSystemConfig.and.returnValue( of(MOCK_SYSTEM_CONFIG_WITH_DATA) diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.ts b/modules/ui/src/app/components/general-settings/general-settings.component.ts index 1d19d0e56..5f34ff98c 100644 --- a/modules/ui/src/app/components/general-settings/general-settings.component.ts +++ b/modules/ui/src/app/components/general-settings/general-settings.component.ts @@ -21,18 +21,19 @@ import { OnInit, Output, } from '@angular/core'; -import { - FormBuilder, - FormControl, - FormGroup, - Validators, -} from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Subject, takeUntil, tap } from 'rxjs'; -import { TestRunService } from '../../services/test-run.service'; +import { + SystemInterfaces, + TestRunService, +} from '../../services/test-run.service'; import { OnlyDifferentValuesValidator } from './only-different-values.validator'; import { CalloutType } from '../../model/callout-type'; import { Observable } from 'rxjs/internal/Observable'; import { shareReplay } from 'rxjs/internal/operators/shareReplay'; +import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { EventType } from '../../model/event-type'; +import { SettingOption } from '../../model/setting'; @Component({ selector: 'app-general-settings', @@ -40,22 +41,34 @@ import { shareReplay } from 'rxjs/internal/operators/shareReplay'; styleUrls: ['./general-settings.component.scss'], }) export class GeneralSettingsComponent implements OnInit, OnDestroy { - @Input() interfaces: string[] = []; + private _interfaces: SystemInterfaces = {}; + @Input() + get interfaces(): SystemInterfaces { + return this._interfaces; + } + set interfaces(value: SystemInterfaces) { + this._interfaces = value; + this.setSystemSetting(); + } @Output() closeSettingEvent = new EventEmitter(); - @Output() openSettingEvent = new EventEmitter(); @Output() reloadInterfacesEvent = new EventEmitter(); public readonly CalloutType = CalloutType; + public readonly EventType = EventType; public settingForm!: FormGroup; public isSubmitting = false; + public defaultInternetOption = { + key: '', + value: 'Not specified', + }; hasConnectionSetting$!: Observable; private destroy$: Subject = new Subject(); get deviceControl(): FormControl { - return this.settingForm.get('device_intf') as FormControl; + return this.settingForm?.get('device_intf') as FormControl; } get internetControl(): FormControl { - return this.settingForm.get('internet_intf') as FormControl; + return this.settingForm?.get('internet_intf') as FormControl; } get isFormValues(): boolean { @@ -67,12 +80,13 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { } get isLessThanTwoInterfaces(): boolean { - return !this.interfaces.length || this.interfaces?.length < 2; + return Object.keys(this.interfaces).length < 2; } constructor( private readonly testRunService: TestRunService, private readonly fb: FormBuilder, + private liveAnnouncer: LiveAnnouncer, private readonly onlyDifferentValuesValidator: OnlyDifferentValuesValidator ) {} @@ -90,9 +104,12 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { this.reloadInterfacesEvent.emit(); } - closeSetting(): void { + closeSetting(message: string): void { this.resetForm(); this.closeSettingEvent.emit(); + this.liveAnnouncer.announce( + `The ${message} finished. The connection setting panel is closed.` + ); this.setSystemSetting(); } @@ -108,8 +125,8 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { private createSettingForm(): FormGroup { return (this.settingForm = this.fb.group( { - device_intf: ['', Validators.required], - internet_intf: ['', Validators.required], + device_intf: [''], + internet_intf: [this.defaultInternetOption], }, { validators: [this.onlyDifferentValuesValidator.onlyDifferentSetting()], @@ -129,23 +146,40 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { this.testRunService.setHasConnectionSetting(true); } else { this.testRunService.setHasConnectionSetting(false); - this.openSetting(); } this.setDefaultFormValues(device_intf, internet_intf); } else { this.testRunService.setHasConnectionSetting(false); - this.openSetting(); } this.testRunService.setSystemConfig(config); }); } + compare(c1: SettingOption, c2: SettingOption): boolean { + return c1 && c2 && c1.key === c2.key && c1.value === c2.value; + } + private setDefaultFormValues( device: string | undefined, internet: string | undefined ): void { - this.deviceControl.setValue(device); - this.internetControl.setValue(internet); + if (device && this.interfaces[device]) { + const deviceData = this.transformValueToObj(device); + this.deviceControl.setValue(deviceData); + } + if (internet && this.interfaces[internet]) { + const interneData = this.transformValueToObj(internet); + this.internetControl.setValue(interneData); + } else { + this.internetControl.setValue(this.defaultInternetOption); + } + } + + private transformValueToObj(value: string): SettingOption { + return { + key: value, + value: this.interfaces[value], + }; } private cleanFormErrorMessage(): void { @@ -161,8 +195,8 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { const { device_intf, internet_intf } = this.settingForm.value; const data = { network: { - device_intf, - internet_intf, + device_intf: device_intf.key, + internet_intf: internet_intf.key, }, }; @@ -170,26 +204,21 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { .createSystemConfig(data) .pipe(takeUntil(this.destroy$)) .subscribe(() => { - this.closeSetting(); + this.closeSetting(EventType.Save); this.testRunService.setSystemConfig(data); - this.testRunService.setIsOpenAddDevice(true); this.testRunService.setHasConnectionSetting(true); }); } - private openSetting(): void { - this.openSettingEvent.emit(); - } - private setSystemSetting(): void { this.testRunService.systemConfig$ .pipe(takeUntil(this.destroy$)) .subscribe(config => { if (config?.network) { const { device_intf, internet_intf } = config.network; - if (device_intf && internet_intf) { - this.setDefaultFormValues(device_intf, internet_intf); - } + this.setDefaultFormValues(device_intf, internet_intf); + } else { + this.internetControl?.setValue(this.defaultInternetOption); } }); } diff --git a/modules/ui/src/app/components/general-settings/only-different-values.validator.ts b/modules/ui/src/app/components/general-settings/only-different-values.validator.ts index dea0c50dc..351e2bc06 100644 --- a/modules/ui/src/app/components/general-settings/only-different-values.validator.ts +++ b/modules/ui/src/app/components/general-settings/only-different-values.validator.ts @@ -39,7 +39,7 @@ export class OnlyDifferentValuesValidator { return null; } - if (deviceControlValue === internetControlValue) { + if (deviceControlValue.key === internetControlValue.key) { return { hasSameValues: true }; } return null; diff --git a/modules/ui/src/app/components/spinner/spinner.component.scss b/modules/ui/src/app/components/spinner/spinner.component.scss index 5358b9874..788f2c676 100644 --- a/modules/ui/src/app/components/spinner/spinner.component.scss +++ b/modules/ui/src/app/components/spinner/spinner.component.scss @@ -9,7 +9,7 @@ top: 0; bottom: 0; background-color: rgba(255, 255, 255, 0.7); - z-index: 9999; + z-index: 2; display: flex; align-items: center; justify-content: center; diff --git a/modules/ui/src/app/components/version/update-dialog/update-dialog.component.html b/modules/ui/src/app/components/version/update-dialog/update-dialog.component.html index 61b507282..b319f9b97 100644 --- a/modules/ui/src/app/components/version/update-dialog/update-dialog.component.html +++ b/modules/ui/src/app/components/version/update-dialog/update-dialog.component.html @@ -25,14 +25,21 @@

- Download diff --git a/modules/ui/src/app/components/version/version.component.html b/modules/ui/src/app/components/version/version.component.html index 7d396e169..2420dbf5e 100644 --- a/modules/ui/src/app/components/version/version.component.html +++ b/modules/ui/src/app/components/version/version.component.html @@ -29,7 +29,7 @@ version?.installed_version + ' New version is available. Click here to update' " - (click)="openUpdateWindow()"> + (click)="openUpdateWindow(version)"> {{ version?.installed_version }} { describe('update is not available', () => { beforeEach(() => { versionBehaviorSubject$.next(VERSION); + mockService.getVersion.and.returnValue(versionBehaviorSubject$); fixture.detectChanges(); }); @@ -64,6 +65,7 @@ describe('VersionComponent', () => { describe('update is available', () => { beforeEach(() => { versionBehaviorSubject$.next(NEW_VERSION); + mockService.getVersion.and.returnValue(versionBehaviorSubject$); fixture.detectChanges(); }); diff --git a/modules/ui/src/app/components/version/version.component.ts b/modules/ui/src/app/components/version/version.component.ts index e102ecaf6..5adb12bd1 100644 --- a/modules/ui/src/app/components/version/version.component.ts +++ b/modules/ui/src/app/components/version/version.component.ts @@ -13,14 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { TestRunService } from '../../services/test-run.service'; import { Version } from '../../model/version'; -import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { UpdateDialogComponent } from './update-dialog/update-dialog.component'; +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'; @Component({ selector: 'app-version', @@ -29,28 +33,47 @@ import { UpdateDialogComponent } from './update-dialog/update-dialog.component'; templateUrl: './version.component.html', styleUrls: ['./version.component.scss'], }) -export class VersionComponent implements OnInit { - version$: BehaviorSubject; +export class VersionComponent implements OnInit, OnDestroy { + version$!: Observable; + private destroy$: Subject = new Subject(); + + private isDialogClosed = false; constructor( private testRunService: TestRunService, public dialog: MatDialog - ) { - this.version$ = testRunService.getVersion(); - } + ) {} ngOnInit() { this.testRunService.fetchVersion(); + + this.version$ = this.testRunService.getVersion().pipe( + tap(version => { + if (version?.update_available && !this.isDialogClosed) { + this.openUpdateWindow(version); + } + }) + ); } - openUpdateWindow() { - this.dialog.open(UpdateDialogComponent, { + openUpdateWindow(version: Version) { + const dialogRef = this.dialog.open(UpdateDialogComponent, { ariaLabel: 'Update version', - data: this.version$.value, + data: version, autoFocus: true, hasBackdrop: true, disableClose: true, panelClass: 'version-update-dialog', }); + + dialogRef + ?.afterClosed() + .pipe(takeUntil(this.destroy$)) + .subscribe(() => (this.isDialogClosed = true)); + } + + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); } } diff --git a/modules/ui/src/app/device-repository/device-form/device-form.component.html b/modules/ui/src/app/device-repository/device-form/device-form.component.html index ef888451a..e8ce4ac98 100644 --- a/modules/ui/src/app/device-repository/device-form/device-form.component.html +++ b/modules/ui/src/app/device-repository/device-form/device-form.component.html @@ -13,7 +13,11 @@ See the License for the specific language governing permissions and limitations under the License. --> -
+ {{ data.title }} Device Manufacturer @@ -79,7 +83,7 @@ ', +}) +class DummyComponent {} + +describe('StateService', () => { + let service: StateService; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DummyComponent], + }); + service = TestBed.inject(StateService); + + fixture = TestBed.createComponent(DummyComponent); + + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should focus element', () => { + const testButton = fixture.nativeElement.querySelector( + '#test-button' + ) as HTMLButtonElement; + + service.focusFirstElementInMain(); + + expect(document.activeElement).toBe(testButton); + }); +}); diff --git a/modules/ui/src/app/services/state.service.ts b/modules/ui/src/app/services/state.service.ts new file mode 100644 index 000000000..509fbd897 --- /dev/null +++ b/modules/ui/src/app/services/state.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class StateService { + focusFirstElementInMain() { + const firstControl: HTMLElement | null = window.document.querySelector( + '#main button:not([disabled="true"]), ' + + '#main a:not([disabled="true"]), #main table' + ); + + if (firstControl) { + firstControl.focus(); + } + } +} diff --git a/modules/ui/src/app/services/test-run.service.spec.ts b/modules/ui/src/app/services/test-run.service.spec.ts index 68b841487..5cb091fed 100644 --- a/modules/ui/src/app/services/test-run.service.spec.ts +++ b/modules/ui/src/app/services/test-run.service.spec.ts @@ -20,7 +20,7 @@ import { import { fakeAsync, getTestBed, TestBed, tick } from '@angular/core/testing'; import { Device, TestModule } from '../model/device'; -import { TestRunService } from './test-run.service'; +import { SystemInterfaces, TestRunService } from './test-run.service'; import { SystemConfig } from '../model/setting'; import { MOCK_PROGRESS_DATA_CANCELLING, @@ -82,11 +82,6 @@ describe('TestRunService', () => { name: 'nmap', enabled: true, }, - { - displayName: 'Security', - name: 'security', - enabled: true, - }, { displayName: 'TLS', name: 'tls', @@ -165,7 +160,10 @@ describe('TestRunService', () => { it('getSystemInterfaces should return array of interfaces', () => { const apiUrl = 'http://localhost:8000/system/interfaces'; - const mockSystemInterfaces: string[] = ['mockValue', 'mockValue']; + const mockSystemInterfaces: SystemInterfaces = { + mockValue1: 'mockValue1', + mockValue2: 'mockValue2', + }; service.getSystemInterfaces().subscribe(res => { expect(res).toEqual(mockSystemInterfaces); @@ -245,32 +243,50 @@ describe('TestRunService', () => { }); }); - it('getHistory should return reports', () => { - let result: TestrunStatus[] | null = null; + describe('getHistory', () => { + it('should return reports', () => { + let result: TestrunStatus[] | null = null; + + const reports = [ + { + status: 'Completed', + device: device, + report: 'https://api.testrun.io/report.pdf', + started: '2023-06-22T10:11:00.123Z', + finished: '2023-06-22T10:17:00.123Z', + }, + ] as TestrunStatus[]; + + service.getHistory().subscribe(res => { + expect(res).toEqual(result); + }); - const reports = [ - { - status: 'Completed', - device: device, - report: 'https://api.testrun.io/report.pdf', - started: '2023-06-22T10:11:00.123Z', - finished: '2023-06-22T10:17:00.123Z', - }, - ] as TestrunStatus[]; + result = reports; + service.fetchHistory(); + const req = httpTestingController.expectOne( + 'http://localhost:8000/reports' + ); - service.getHistory().subscribe(res => { - expect(res).toEqual(result); + expect(req.request.method).toBe('GET'); + + req.flush(reports); }); - result = reports; - service.fetchHistory(); - const req = httpTestingController.expectOne( - 'http://localhost:8000/reports' - ); + it('should return [] when error happens', () => { + let result: TestrunStatus[] | null = null; - expect(req.request.method).toBe('GET'); + service.getHistory().subscribe(res => { + expect(res).toEqual(result); + }); - req.flush(reports); + result = []; + service.fetchHistory(); + const req = httpTestingController.expectOne({ + url: 'http://localhost:8000/reports', + }); + + req.flush([], { status: 500, statusText: 'error' }); + }); }); describe('#getResultClass', () => { @@ -435,4 +451,32 @@ describe('TestRunService', () => { ) ).toEqual(false); })); + + it('#saveDevice should have necessary request data', () => { + const apiUrl = 'http://localhost:8000/device'; + + service.saveDevice(device).subscribe(res => { + expect(res).toEqual(true); + }); + + const req = httpTestingController.expectOne(apiUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(JSON.stringify(device)); + req.flush(true); + }); + + it('#editDevice should have necessary request data', () => { + const apiUrl = 'http://localhost:8000/device/edit'; + + service.editDevice(device, '01:01:01:01:01:01').subscribe(res => { + expect(res).toEqual(true); + }); + + const req = httpTestingController.expectOne(apiUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual( + JSON.stringify({ mac_addr: '01:01:01:01:01:01', device }) + ); + req.flush(true); + }); }); diff --git a/modules/ui/src/app/services/test-run.service.ts b/modules/ui/src/app/services/test-run.service.ts index 22b4d2328..37d46f23b 100644 --- a/modules/ui/src/app/services/test-run.service.ts +++ b/modules/ui/src/app/services/test-run.service.ts @@ -30,6 +30,10 @@ import { Version } from '../model/version'; const API_URL = 'http://localhost:8000'; +export type SystemInterfaces = { + [key: string]: string; +}; + @Injectable({ providedIn: 'root', }) @@ -55,11 +59,6 @@ export class TestRunService { name: 'nmap', enabled: true, }, - { - displayName: 'Security', - name: 'security', - enabled: true, - }, { displayName: 'TLS', name: 'tls', @@ -70,6 +69,8 @@ export class TestRunService { private devices = new BehaviorSubject(null); private isOpenAddDeviceSub$ = new BehaviorSubject(false); public isOpenAddDevice$ = this.isOpenAddDeviceSub$.asObservable(); + private isOpenStartTestrunSub$ = new BehaviorSubject(false); + public isOpenStartTestrun$ = this.isOpenStartTestrunSub$.asObservable(); private _systemConfig = new BehaviorSubject(null); public systemConfig$ = this._systemConfig.asObservable(); private systemStatusSubject = new ReplaySubject(1); @@ -87,6 +88,10 @@ export class TestRunService { this.isOpenAddDeviceSub$.next(isOpen); } + setIsOpenStartTestrun(isOpen: boolean): void { + this.isOpenStartTestrunSub$.next(isOpen); + } + setHasConnectionSetting(hasSetting: boolean): void { this.hasConnectionSettingSub$.next(hasSetting); } @@ -124,8 +129,8 @@ export class TestRunService { .pipe(retry(1)); } - getSystemInterfaces(): Observable { - return this.http.get(`${API_URL}/system/interfaces`); + getSystemInterfaces(): Observable { + return this.http.get(`${API_URL}/system/interfaces`); } /** @@ -161,6 +166,20 @@ export class TestRunService { .pipe(map(() => true)); } + editDevice(device: Device, mac_addr: string): Observable { + type EditDeviceRequest = { + mac_addr: string; // original mac address + device: Device; + }; + const request: EditDeviceRequest = { + mac_addr, + device, + }; + + return this.http + .post(`${API_URL}/device/edit`, JSON.stringify(request)) + .pipe(map(() => true)); + } deleteDevice(device: Device): Observable { return this.http .delete(`${API_URL}/device`, { @@ -185,12 +204,13 @@ export class TestRunService { updateDevice(deviceToUpdate: Device, update: Device): void { const device = this.devices.value?.find( - device => update.mac_addr === device.mac_addr + device => deviceToUpdate.mac_addr === device.mac_addr ); if (device) { device.model = update.model; device.manufacturer = update.manufacturer; device.test_modules = update.test_modules; + device.mac_addr = update.mac_addr; this.devices.next(this.devices.value); } @@ -207,12 +227,14 @@ export class TestRunService { } fetchHistory(): void { - this.http - .get(`${API_URL}/reports`) - .pipe(retry(1)) - .subscribe(data => { + this.http.get(`${API_URL}/reports`).subscribe( + data => { this.history.next(data); - }); + }, + () => { + this.history.next([]); + } + ); } getHistory(): BehaviorSubject { diff --git a/modules/ui/src/index.html b/modules/ui/src/index.html index 37521c779..aee186784 100644 --- a/modules/ui/src/index.html +++ b/modules/ui/src/index.html @@ -16,6 +16,20 @@ + + +