diff --git a/README.md b/README.md index 53915aaaa..ccd056c2e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ [![CodeQL](https://github.com/google/testrun/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)](https://github.com/google/testrun/actions/workflows/github-code-scanning/codeql) [![Testrun test suite](https://github.com/google/testrun/actions/workflows/testing.yml/badge.svg?branch=main&event=push)](https://github.com/google/testrun/actions/workflows/testing.yml) +Disclaimer: Testrun uses Google Analytics to learn about how our users use Testrun. By installing and running Testrun, you understand and accept the Terms of Service found [here](https://policies.google.com/technologies/partner-sites). + ## Introduction :wave: Testrun automates specific test cases to verify network and security functionality in IoT devices. It is an open source tool which allows manufacturers of IP capable devices to test their devices for the purposes of Device Qualification within the BOS program. 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 351dde110..a40e6ba46 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. -"""Manages the Testrun API""" +"""Provides Testrun data via REST API.""" from fastapi import FastAPI, APIRouter, Response, Request, status from fastapi.responses import FileResponse @@ -39,7 +39,7 @@ DEFAULT_DEVICE_INTF = "enx123456789123" LATEST_RELEASE_CHECK = ("https://api.github.com/repos/google/" + - "testrun/releases/latest") + "testrun/releases/latest") class Api: """Provide REST endpoints to manage Testrun""" @@ -126,14 +126,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 @@ -255,32 +258,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 @@ -460,7 +469,7 @@ async def edit_device(self, request: Request, response: Response): 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 + 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, @@ -483,6 +492,7 @@ async def edit_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 85f9041e4..69aaaff5d 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -43,6 +43,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() @@ -238,9 +239,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 @@ -248,8 +256,6 @@ def reset(self): def to_json(self): - # TODO: Add report URL - results = { 'total': self.get_total_tests(), 'results': self.get_test_results() @@ -263,6 +269,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 6528557c5..f46c0b225 100644 --- a/framework/python/src/common/testreport.py +++ b/framework/python/src/common/testreport.py @@ -81,6 +81,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 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/net_orc/network_validator.py b/framework/python/src/net_orc/network_validator.py index 5f3bec22d..6673a1fdb 100644 --- a/framework/python/src/net_orc/network_validator.py +++ b/framework/python/src/net_orc/network_validator.py @@ -240,8 +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 ad5a739c9..e8b376966 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.get_session().set_report_url(report.get_report_url()) # Move testing output from runtime to local device folder timestamp_dir = self._timestamp_results(device) @@ -240,6 +241,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), 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 56dea19a0..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) + rdpcap(NTP_SERVER_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) + rdpcap(NTP_SERVER_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.component.html b/modules/ui/src/app/app.component.html index 86281676f..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,18 +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 menu there." + aria-label="The Create a Device link redirects to the Devices page and opens the dialogue there." tabindex="0" role="link" class="message-link" @@ -132,7 +134,7 @@

Testrun

(click)="navigateToRuntime()" (keydown.enter)="navigateToRuntime()" (keydown.space)="navigateToRuntime()" - aria-label="The Testrun link redirects to the Testrun page and opens a menu there." + aria-label="The Testrun link redirects to the Testrun page and opens the dialogue there." tabindex="0" role="link" class="message-link" diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index 69e921323..093ad3f35 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -45,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; @@ -53,6 +54,7 @@ describe('AppComponent', () => { let router: Router; let mockService: SpyObj; let mockLoaderService: SpyObj; + let mockStateService: SpyObj; const enterKeyEvent = new KeyboardEvent('keydown', { key: 'Enter', @@ -82,6 +84,10 @@ describe('AppComponent', () => { ]); mockLoaderService = jasmine.createSpyObj(['setLoading']); + mockStateService = jasmine.createSpyObj('mockStateService', [ + 'focusFirstElementInMain', + ]); + mockService.getDevices.and.returnValue( new BehaviorSubject([device]) ); @@ -106,6 +112,7 @@ describe('AppComponent', () => { providers: [ { provide: TestRunService, useValue: mockService }, { provide: LoaderService, useValue: mockLoaderService }, + { provide: StateService, useValue: mockStateService }, ], declarations: [ AppComponent, @@ -226,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'); diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index 11f39be81..287d8ba20 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -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'; @@ -59,6 +60,7 @@ export class AppComponent implements OnInit { 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(); @@ -139,11 +142,15 @@ export class AppComponent implements OnInit { 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 { @@ -175,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/device-item/device-item.component.html b/modules/ui/src/app/components/device-item/device-item.component.html index 7fc675c86..0496d74fb 100644 --- a/modules/ui/src/app/components/device-item/device-item.component.html +++ b/modules/ui/src/app/components/device-item/device-item.component.html @@ -14,9 +14,11 @@ limitations under the License. --> + +
+ + +
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 b7025ff76..d636b4727 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 @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@use '@angular/material' as mat; @import '../../../theming/colors'; @import '../../../theming/variables'; @@ -22,39 +23,103 @@ $border-radius: 12px; .device-item { display: grid; width: $device-item-width; + height: 80px; border-radius: $border-radius; border: 1px solid #c4c7c5; background: $white; box-sizing: border-box; - grid-template-columns: 1fr 1fr $icon-width; + grid-template-columns: 1fr 1fr; padding: 0; grid-column-gap: 8px; grid-row-gap: 4px; font-family: 'Open Sans', sans-serif; grid-template-areas: - 'manufacturer manufacturer icon' - 'name address icon'; + 'manufacturer manufacturer' + 'name address'; &:hover { cursor: pointer; } + + &.non-interective { + &:hover { + cursor: default; + } + } +} + +.device-item-with-actions { + display: grid; + width: $device-item-width; + border-radius: $border-radius; + border: 1px solid #c4c7c5; + background: $white; + box-sizing: border-box; + grid-template-columns: 1fr $icon-width; + grid-column-gap: 1px; + padding: 0; + font-family: 'Open Sans', sans-serif; + grid-template-areas: 'edit start'; +} + +.button-edit { + display: grid; + grid-area: edit; + background: $white; + box-sizing: border-box; + grid-template-columns: 1fr 1fr; + padding: 0 6px 0 0; + grid-column-gap: 8px; + grid-row-gap: 4px; + font-family: 'Open Sans', sans-serif; + grid-template-areas: + 'manufacturer manufacturer' + 'name address'; + border-radius: $border-radius 0 0 $border-radius; + border: none; + + &:hover { + cursor: pointer; + + .item-manufacturer-text { + max-width: 206px; + } + + .item-manufacturer-icon { + visibility: visible; + } + } } .item-manufacturer { + display: flex; padding: 0 16px; grid-area: manufacturer; justify-self: start; align-self: end; color: #1f1f1f; - justify-content: start; + justify-content: flex-start; font-size: 16px; font-weight: 500; line-height: 24px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - width: 230px; text-align: start; + gap: 4px; + + .item-manufacturer-text { + margin: 0; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + max-width: 230px; + } + + .item-manufacturer-icon { + display: flex; + visibility: hidden; + font-size: 20px; + align-items: center; + justify-content: center; + } } .item-name { @@ -83,13 +148,27 @@ $border-radius: 12px; line-height: 20px; } -.item-icon { - grid-area: icon; +.button-start { + grid-area: start; width: $icon-width; height: calc($icon-width - 2px); - background-color: #e8f0fe; + background-color: mat.get-color-from-palette($color-primary, 50); justify-self: end; - border-top-right-radius: $border-radius; - border-bottom-right-radius: $border-radius; - background-image: url(/assets/icons/devices_add.svg); + border-radius: 0 $border-radius $border-radius 0; + + &:hover, + &:focus-visible { + background-color: mat.get-color-from-palette($color-primary, 600); + + .button-start-icon { + color: $white; + } + } +} + +.button-start-icon { + margin: 0; + width: 24px; + height: 24px; + font-size: 24px; } diff --git a/modules/ui/src/app/components/device-item/device-item.component.spec.ts b/modules/ui/src/app/components/device-item/device-item.component.spec.ts index 2105fd66e..e94188040 100644 --- a/modules/ui/src/app/components/device-item/device-item.component.spec.ts +++ b/modules/ui/src/app/components/device-item/device-item.component.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Device } from '../../model/device'; +import { Device, DeviceView } from '../../model/device'; import { DeviceItemComponent } from './device-item.component'; import { DeviceRepositoryModule } from '../../device-repository/device-repository.module'; @@ -48,29 +48,59 @@ describe('DeviceItemComponent', () => { expect(component).toBeTruthy(); }); - it('should display information about device', () => { - const name = compiled.querySelector('.item-name'); - const manufacturer = compiled.querySelector('.item-manufacturer'); - const mac = compiled.querySelector('.item-mac-address'); + describe('with device view as Basic', () => { + beforeEach(() => { + component.deviceView = DeviceView.Basic; + fixture.detectChanges(); + }); - expect(name?.textContent?.trim()).toEqual('O3-DIN-CPU'); - expect(manufacturer?.textContent?.trim()).toEqual('Delta'); - expect(mac?.textContent?.trim()).toEqual('00:1e:42:35:73:c4'); - }); + it('should display information about device', () => { + const name = compiled.querySelector('.item-name'); + const manufacturer = compiled.querySelector('.item-manufacturer'); + const mac = compiled.querySelector('.item-mac-address'); + + expect(name?.textContent?.trim()).toEqual('O3-DIN-CPU'); + expect(manufacturer?.textContent?.trim()).toEqual('Delta'); + expect(mac?.textContent?.trim()).toEqual('00:1e:42:35:73:c4'); + }); - it('should emit mac address', () => { - const clickSpy = spyOn(component.itemClicked, 'emit'); - const item = compiled.querySelector('.device-item') as HTMLElement; - item.click(); + it('should emit mac address', () => { + const clickSpy = spyOn(component.itemClicked, 'emit'); + const item = compiled.querySelector('.device-item') as HTMLElement; + item.click(); - expect(clickSpy).toHaveBeenCalledWith(component.device); + expect(clickSpy).toHaveBeenCalledWith(component.device); + }); + + it('should have tabindex', () => { + component.tabIndex = -2; + fixture.detectChanges(); + const item = compiled.querySelector('.device-item') as HTMLElement; + + expect(item.tabIndex).toBe(-2); + }); }); - it('should have tabindex', () => { - component.tabIndex = -2; - fixture.detectChanges(); - const item = compiled.querySelector('.device-item') as HTMLElement; + describe('with device view as WithActions', () => { + beforeEach(() => { + component.deviceView = DeviceView.WithActions; + fixture.detectChanges(); + }); - expect(item.tabIndex).toBe(-2); + it('should emit device on click edit button', () => { + const clickSpy = spyOn(component.itemClicked, 'emit'); + const editBtn = compiled.querySelector('.button-edit') as HTMLElement; + editBtn.click(); + + expect(clickSpy).toHaveBeenCalledWith(component.device); + }); + + it('should emit device on click start button', () => { + const clickSpy = spyOn(component.startTestrunClicked, 'emit'); + const editBtn = compiled.querySelector('.button-start') as HTMLElement; + editBtn.click(); + + expect(clickSpy).toHaveBeenCalledWith(component.device); + }); }); }); 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..2b1c44e0f 100644 --- a/modules/ui/src/app/components/device-item/device-item.component.ts +++ b/modules/ui/src/app/components/device-item/device-item.component.ts @@ -14,24 +14,34 @@ * limitations under the License. */ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { Device } from '../../model/device'; +import { Device, DeviceView } from '../../model/device'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; @Component({ selector: 'app-device-item', templateUrl: './device-item.component.html', styleUrls: ['./device-item.component.scss'], standalone: true, + imports: [CommonModule, MatButtonModule, MatIconModule], }) export class DeviceItemComponent { @Input() device!: Device; @Input() tabIndex = 0; + @Input() deviceView!: string; @Output() itemClicked = new EventEmitter(); + @Output() startTestrunClicked = new EventEmitter(); + readonly DeviceView = DeviceView; itemClick(): void { this.itemClicked.emit(this.device); } + startTestrunClick(): void { + this.startTestrunClicked.emit(this.device); + } get label() { - return `${this.device.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/general-settings/general-settings.component.html b/modules/ui/src/app/components/general-settings/general-settings.component.html index 60d66f116..c698e167f 100644 --- a/modules/ui/src/app/components/general-settings/general-settings.component.html +++ b/modules/ui/src/app/components/general-settings/general-settings.component.html @@ -16,8 +16,7 @@

Connection settings

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

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

- - - {{ interface.value }} - - - -

- 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.value }} - - + + 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 30a6c114d..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 { 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 c365e8557..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 @@ -16,7 +16,10 @@ 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'; @@ -25,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: { @@ -35,15 +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_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 () => { @@ -54,6 +68,7 @@ describe('GeneralSettingsComponent', () => { 'createSystemConfig', 'hasConnectionSetting$', 'setHasConnectionSetting', + 'systemConfig$', ]); testRunServiceMock.getSystemInterfaces.and.returnValue(of({})); testRunServiceMock.getSystemConfig.and.returnValue( @@ -62,8 +77,11 @@ describe('GeneralSettingsComponent', () => { 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, @@ -71,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(); @@ -95,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 ); }); @@ -117,29 +142,48 @@ describe('GeneralSettingsComponent', () => { 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'); - expect(component.settingForm.value).toEqual( - MOCK_SYSTEM_CONFIG_WITH_DATA.network + const expectedDevice = { key: 'mockDeviceKey', value: 'mockDeviceValue' }; + const expectedInternet = { + key: 'mockInternetKey', + value: 'mockInternetValue', + }; + + expect(component.settingForm.value.device_intf).toEqual(expectedDevice); + + expect(component.settingForm.value.internet_intf).toEqual( + expectedInternet ); }); }); @@ -162,18 +206,24 @@ 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 + expectedResult ); }); }); 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 14a02c6b1..c26c535da 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,12 +21,7 @@ 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 { SystemInterfaces, @@ -36,6 +31,9 @@ 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', @@ -43,12 +41,25 @@ import { shareReplay } from 'rxjs/internal/operators/shareReplay'; styleUrls: ['./general-settings.component.scss'], }) export class GeneralSettingsComponent implements OnInit, OnDestroy { - @Input() interfaces: SystemInterfaces = {}; + private _interfaces: SystemInterfaces = {}; + @Input() + get interfaces(): SystemInterfaces { + return this._interfaces; + } + set interfaces(value: SystemInterfaces) { + this._interfaces = value; + this.setSystemSetting(); + } @Output() closeSettingEvent = 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(); @@ -75,6 +86,7 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { constructor( private readonly testRunService: TestRunService, private readonly fb: FormBuilder, + private liveAnnouncer: LiveAnnouncer, private readonly onlyDifferentValuesValidator: OnlyDifferentValuesValidator ) {} @@ -92,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(); } @@ -110,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()], @@ -140,12 +155,31 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { }); } + 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,7 +204,7 @@ 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.setHasConnectionSetting(true); }); @@ -182,9 +216,7 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { .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); } }); } 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/version/version.component.ts b/modules/ui/src/app/components/version/version.component.ts index bd2174623..5adb12bd1 100644 --- a/modules/ui/src/app/components/version/version.component.ts +++ b/modules/ui/src/app/components/version/version.component.ts @@ -13,7 +13,7 @@ * 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'; @@ -23,6 +23,8 @@ 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', @@ -31,8 +33,11 @@ import { Observable } from 'rxjs/internal/Observable'; templateUrl: './version.component.html', styleUrls: ['./version.component.scss'], }) -export class VersionComponent implements OnInit { +export class VersionComponent implements OnInit, OnDestroy { version$!: Observable; + private destroy$: Subject = new Subject(); + + private isDialogClosed = false; constructor( private testRunService: TestRunService, @@ -44,7 +49,7 @@ export class VersionComponent implements OnInit { this.version$ = this.testRunService.getVersion().pipe( tap(version => { - if (version?.update_available) { + if (version?.update_available && !this.isDialogClosed) { this.openUpdateWindow(version); } }) @@ -52,7 +57,7 @@ export class VersionComponent implements OnInit { } openUpdateWindow(version: Version) { - this.dialog.open(UpdateDialogComponent, { + const dialogRef = this.dialog.open(UpdateDialogComponent, { ariaLabel: 'Update version', data: version, autoFocus: true, @@ -60,5 +65,15 @@ export class VersionComponent implements OnInit { 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 be653726f..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 diff --git a/modules/ui/src/app/device-repository/device-repository.component.html b/modules/ui/src/app/device-repository/device-repository.component.html index 8ce001edb..77eb6451c 100644 --- a/modules/ui/src/app/device-repository/device-repository.component.html +++ b/modules/ui/src/app/device-repository/device-repository.component.html @@ -22,6 +22,8 @@

Devices

diff --git a/modules/ui/src/app/device-repository/device-repository.component.spec.ts b/modules/ui/src/app/device-repository/device-repository.component.spec.ts index 9dd8f1340..3bcc131f3 100644 --- a/modules/ui/src/app/device-repository/device-repository.component.spec.ts +++ b/modules/ui/src/app/device-repository/device-repository.component.spec.ts @@ -30,6 +30,7 @@ import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { device } from '../mocks/device.mock'; import { DeleteFormComponent } from '../components/delete-form/delete-form.component'; import SpyObj = jasmine.SpyObj; +import { StateService } from '../services/state.service'; describe('DeviceRepositoryComponent', () => { let component: DeviceRepositoryComponent; @@ -37,6 +38,11 @@ describe('DeviceRepositoryComponent', () => { let compiled: HTMLElement; let mockService: SpyObj; + const stateServiceMock: jasmine.SpyObj = jasmine.createSpyObj( + 'stateServiceMock', + ['focusFirstElementInMain'] + ); + beforeEach(() => { mockService = jasmine.createSpyObj([ 'getDevices', @@ -67,7 +73,10 @@ describe('DeviceRepositoryComponent', () => { ]); TestBed.configureTestingModule({ imports: [DeviceRepositoryModule, BrowserAnimationsModule], - providers: [{ provide: TestRunService, useValue: mockService }], + providers: [ + { provide: TestRunService, useValue: mockService }, + { provide: StateService, useValue: stateServiceMock }, + ], declarations: [DeviceRepositoryComponent], }); fixture = TestBed.createComponent(DeviceRepositoryComponent); diff --git a/modules/ui/src/app/device-repository/device-repository.component.ts b/modules/ui/src/app/device-repository/device-repository.component.ts index 7d35221d3..5e513cd6e 100644 --- a/modules/ui/src/app/device-repository/device-repository.component.ts +++ b/modules/ui/src/app/device-repository/device-repository.component.ts @@ -22,7 +22,7 @@ import { } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Observable } from 'rxjs/internal/Observable'; -import { Device } from '../model/device'; +import { Device, DeviceView } from '../model/device'; import { TestRunService } from '../services/test-run.service'; import { DeviceFormComponent, @@ -32,6 +32,11 @@ import { import { Subject, takeUntil } from 'rxjs'; import { DeleteFormComponent } from '../components/delete-form/delete-form.component'; import { combineLatest } from 'rxjs/internal/observable/combineLatest'; +import { ProgressInitiateFormComponent } from '../progress/progress-initiate-form/progress-initiate-form.component'; +import { Routes } from '../model/routes'; +import { Router } from '@angular/router'; +import { StateService } from '../services/state.service'; +import { timer } from 'rxjs/internal/observable/timer'; @Component({ selector: 'app-device-repository', @@ -40,14 +45,17 @@ import { combineLatest } from 'rxjs/internal/observable/combineLatest'; }) export class DeviceRepositoryComponent implements OnInit, OnDestroy { devices$!: Observable; + readonly DeviceView = DeviceView; private destroy$: Subject = new Subject(); selectedDevice: Device | undefined | null; constructor( private testRunService: TestRunService, + private readonly state: StateService, public dialog: MatDialog, private element: ElementRef, - private readonly changeDetectorRef: ChangeDetectorRef + private readonly changeDetectorRef: ChangeDetectorRef, + private route: Router ) {} ngOnInit(): void { @@ -67,6 +75,28 @@ export class DeviceRepositoryComponent implements OnInit, OnDestroy { this.destroy$.unsubscribe(); } + openStartTestrun(selectedDevice: Device): void { + const dialogRef = this.dialog.open(ProgressInitiateFormComponent, { + ariaLabel: 'Initiate testrun', + data: { + device: selectedDevice, + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: 'initiate-test-run-dialog', + }); + + dialogRef + ?.afterClosed() + .pipe(takeUntil(this.destroy$)) + .subscribe(data => { + if (data === 'testrunStarted') { + this.route.navigate([Routes.Testrun]); + } + }); + } + openDialog(selectedDevice?: Device, focusDeleteButton = false): void { const dialogRef = this.dialog.open(DeviceFormComponent, { ariaLabel: selectedDevice ? 'Edit device' : 'Create device', @@ -93,6 +123,11 @@ export class DeviceRepositoryComponent implements OnInit, OnDestroy { !selectedDevice ) { this.testRunService.addDevice(response.device); + timer(10) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.state.focusFirstElementInMain(); + }); } if ( response.action === FormAction.Save && diff --git a/modules/ui/src/app/model/device.ts b/modules/ui/src/app/model/device.ts index 1ac0b8b28..ba526e661 100644 --- a/modules/ui/src/app/model/device.ts +++ b/modules/ui/src/app/model/device.ts @@ -38,3 +38,8 @@ export interface TestModule { name: string; enabled: boolean; } + +export enum DeviceView { + Basic = 'basic', + WithActions = 'with actions', +} diff --git a/modules/ui/src/app/model/event-type.ts b/modules/ui/src/app/model/event-type.ts new file mode 100644 index 000000000..617fe8a49 --- /dev/null +++ b/modules/ui/src/app/model/event-type.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export enum EventType { + Close = 'Close event', + Save = 'Save event', +} diff --git a/modules/ui/src/app/model/setting.ts b/modules/ui/src/app/model/setting.ts index 5d9c768e0..2fd7e7875 100644 --- a/modules/ui/src/app/model/setting.ts +++ b/modules/ui/src/app/model/setting.ts @@ -19,3 +19,8 @@ export interface SystemConfig { internet_intf?: string; }; } + +export interface SettingOption { + key: string; + value: string; +} diff --git a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.html b/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.html index b09a53581..a5b8e1289 100644 --- a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.html +++ b/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.html @@ -22,6 +22,7 @@ @@ -29,6 +30,7 @@ - +