aboutsummaryrefslogtreecommitdiffstats
path: root/resources/tools/dash/app
diff options
context:
space:
mode:
Diffstat (limited to 'resources/tools/dash/app')
-rw-r--r--resources/tools/dash/app/.ebextensions/cron-linux.config14
-rw-r--r--resources/tools/dash/app/Procfile1
-rw-r--r--resources/tools/dash/app/app.ini20
-rw-r--r--resources/tools/dash/app/config.py27
-rw-r--r--resources/tools/dash/app/pal/__init__.py69
-rw-r--r--resources/tools/dash/app/pal/data/__init__.py12
-rw-r--r--resources/tools/dash/app/pal/data/data.py351
-rw-r--r--resources/tools/dash/app/pal/data/data.yaml117
-rw-r--r--resources/tools/dash/app/pal/debug.py45
-rw-r--r--resources/tools/dash/app/pal/news/__init__.py12
-rw-r--r--resources/tools/dash/app/pal/news/layout.py707
-rw-r--r--resources/tools/dash/app/pal/news/news.py46
-rw-r--r--resources/tools/dash/app/pal/news/tables.py85
-rw-r--r--resources/tools/dash/app/pal/report/__init__.py12
-rw-r--r--resources/tools/dash/app/pal/report/graphs.py275
-rw-r--r--resources/tools/dash/app/pal/report/layout.py1446
-rw-r--r--resources/tools/dash/app/pal/report/layout.yaml240
-rw-r--r--resources/tools/dash/app/pal/report/report.py48
-rw-r--r--resources/tools/dash/app/pal/routes.py32
-rw-r--r--resources/tools/dash/app/pal/static/dist/css/bootstrap.min.css12
-rw-r--r--resources/tools/dash/app/pal/static/dist/img/favicon.svg348
-rw-r--r--resources/tools/dash/app/pal/static/img/logo.svg348
-rw-r--r--resources/tools/dash/app/pal/stats/__init__.py12
-rw-r--r--resources/tools/dash/app/pal/stats/graphs.py138
-rw-r--r--resources/tools/dash/app/pal/stats/layout.py920
-rw-r--r--resources/tools/dash/app/pal/stats/layout.yaml117
-rw-r--r--resources/tools/dash/app/pal/stats/stats.py48
-rw-r--r--resources/tools/dash/app/pal/templates/base_layout.jinja222
-rw-r--r--resources/tools/dash/app/pal/templates/index_layout.jinja234
-rw-r--r--resources/tools/dash/app/pal/templates/news_layout.jinja217
-rw-r--r--resources/tools/dash/app/pal/templates/report_layout.jinja217
-rw-r--r--resources/tools/dash/app/pal/templates/stats_layout.jinja217
-rw-r--r--resources/tools/dash/app/pal/templates/trending_layout.jinja217
-rw-r--r--resources/tools/dash/app/pal/trending/__init__.py12
-rw-r--r--resources/tools/dash/app/pal/trending/graphs.py417
-rw-r--r--resources/tools/dash/app/pal/trending/layout.py1393
-rw-r--r--resources/tools/dash/app/pal/trending/layout.yaml210
-rw-r--r--resources/tools/dash/app/pal/trending/trending.py48
-rw-r--r--resources/tools/dash/app/pal/utils/__init__.py12
-rw-r--r--resources/tools/dash/app/pal/utils/constants.py312
-rw-r--r--resources/tools/dash/app/pal/utils/tooltips.yaml40
-rw-r--r--resources/tools/dash/app/pal/utils/url_processing.py99
-rw-r--r--resources/tools/dash/app/pal/utils/utils.py344
-rw-r--r--resources/tools/dash/app/requirements.txt39
-rw-r--r--resources/tools/dash/app/wsgi.py21
45 files changed, 0 insertions, 8573 deletions
diff --git a/resources/tools/dash/app/.ebextensions/cron-linux.config b/resources/tools/dash/app/.ebextensions/cron-linux.config
deleted file mode 100644
index ae8c33c814..0000000000
--- a/resources/tools/dash/app/.ebextensions/cron-linux.config
+++ /dev/null
@@ -1,14 +0,0 @@
-files:
- "/etc/cron.d/mycron":
- mode: "000644"
- owner: root
- group: root
- content: |
- SHELL=/bin/bash
- PATH=/sbin:/bin:/usr/sbin:/usr/bin
- MAILTO=root
- 0 6 * * * root /bin/echo 'c' > /tmp/masterfifo
-
-commands:
- remove_old_cron:
- command: "rm -f /etc/cron.d/mycron.bak" \ No newline at end of file
diff --git a/resources/tools/dash/app/Procfile b/resources/tools/dash/app/Procfile
deleted file mode 100644
index c79d502390..0000000000
--- a/resources/tools/dash/app/Procfile
+++ /dev/null
@@ -1 +0,0 @@
-uwsgi: uwsgi app.ini
diff --git a/resources/tools/dash/app/app.ini b/resources/tools/dash/app/app.ini
deleted file mode 100644
index 9608f7c38d..0000000000
--- a/resources/tools/dash/app/app.ini
+++ /dev/null
@@ -1,20 +0,0 @@
-[uwsgi]
-ini = :pal
-py-autoreload = 0
-
-[pal]
-module = wsgi:app
-master-fifo = /tmp/masterfifo
-lazy = True
-lazy-apps = True
-touch-chain-reload
-listen = 128
-
-workers = 2
-plugin = python3
-
-master = true
-http-socket = :5000
-socket = /tmp/app.sock
-chmod-socket = 666
-
diff --git a/resources/tools/dash/app/config.py b/resources/tools/dash/app/config.py
deleted file mode 100644
index c927c3ca50..0000000000
--- a/resources/tools/dash/app/config.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-class Config:
- """Flask configuration variables.
- """
-
- # General Config
- FLASK_APP = "wsgi.py"
- FLASK_ENV = "production"
-
- # Static Assets
- STATIC_FOLDER = "static"
- TEMPLATES_FOLDER = "templates"
- COMPRESSOR_DEBUG ="True"
diff --git a/resources/tools/dash/app/pal/__init__.py b/resources/tools/dash/app/pal/__init__.py
deleted file mode 100644
index 0eb2a4e79e..0000000000
--- a/resources/tools/dash/app/pal/__init__.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Initialize Flask app.
-"""
-
-import logging
-
-from flask import Flask
-from flask_assets import Environment
-
-from .utils.constants import Constants as C
-
-
-def init_app():
- """Construct core Flask application with embedded Dash app.
- """
-
- logging.basicConfig(
- format=u"%(asctime)s: %(levelname)s: %(message)s",
- datefmt=u"%Y/%m/%d %H:%M:%S",
- level=logging.INFO
- )
-
- logging.info("Application started.")
-
- app = Flask(__name__, instance_relative_config=False)
- app.config.from_object(u"config.Config")
-
- with app.app_context():
- # Import parts of our core Flask app.
- from . import routes
-
- assets = Environment()
- assets.init_app(app)
-
- # Set the time period for Trending
- if C.TIME_PERIOD is None or C.TIME_PERIOD > C.MAX_TIME_PERIOD:
- time_period = C.MAX_TIME_PERIOD
- else:
- time_period = C.TIME_PERIOD
-
- # Import Dash applications.
- from .news.news import init_news
- app = init_news(app)
-
- from .stats.stats import init_stats
- app = init_stats(app, time_period=time_period)
-
- from .trending.trending import init_trending
- app = init_trending(app, time_period=time_period)
-
- from .report.report import init_report
- app = init_report(app, releases=C.RELEASES)
-
- return app
-
-
-app = init_app()
diff --git a/resources/tools/dash/app/pal/data/__init__.py b/resources/tools/dash/app/pal/data/__init__.py
deleted file mode 100644
index 5692432123..0000000000
--- a/resources/tools/dash/app/pal/data/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
diff --git a/resources/tools/dash/app/pal/data/data.py b/resources/tools/dash/app/pal/data/data.py
deleted file mode 100644
index 77fd113a9c..0000000000
--- a/resources/tools/dash/app/pal/data/data.py
+++ /dev/null
@@ -1,351 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Prepare data for Plotly Dash applications.
-"""
-
-import logging
-import awswrangler as wr
-
-from yaml import load, FullLoader, YAMLError
-from datetime import datetime, timedelta
-from time import time
-from pytz import UTC
-from pandas import DataFrame
-from awswrangler.exceptions import EmptyDataFrame, NoFilesFound
-
-
-class Data:
- """Gets the data from parquets and stores it for further use by dash
- applications.
- """
-
- def __init__(self, data_spec_file: str, debug: bool=False) -> None:
- """Initialize the Data object.
-
- :param data_spec_file: Path to file specifying the data to be read from
- parquets.
- :param debug: If True, the debuf information is printed to stdout.
- :type data_spec_file: str
- :type debug: bool
- :raises RuntimeError: if it is not possible to open data_spec_file or it
- is not a valid yaml file.
- """
-
- # Inputs:
- self._data_spec_file = data_spec_file
- self._debug = debug
-
- # Specification of data to be read from parquets:
- self._data_spec = None
-
- # Data frame to keep the data:
- self._data = None
-
- # Read from files:
- try:
- with open(self._data_spec_file, "r") as file_read:
- self._data_spec = load(file_read, Loader=FullLoader)
- except IOError as err:
- raise RuntimeError(
- f"Not possible to open the file {self._data_spec_file,}\n{err}"
- )
- except YAMLError as err:
- raise RuntimeError(
- f"An error occurred while parsing the specification file "
- f"{self._data_spec_file,}\n"
- f"{err}"
- )
-
- @property
- def data(self):
- return self._data
-
- def _get_columns(self, parquet: str) -> list:
- """Get the list of columns from the data specification file to be read
- from parquets.
-
- :param parquet: The parquet's name.
- :type parquet: str
- :raises RuntimeError: if the parquet is not defined in the data
- specification file or it does not have any columns specified.
- :returns: List of columns.
- :rtype: list
- """
-
- try:
- return self._data_spec[parquet]["columns"]
- except KeyError as err:
- raise RuntimeError(
- f"The parquet {parquet} is not defined in the specification "
- f"file {self._data_spec_file} or it does not have any columns "
- f"specified.\n{err}"
- )
-
- def _get_path(self, parquet: str) -> str:
- """Get the path from the data specification file to be read from
- parquets.
-
- :param parquet: The parquet's name.
- :type parquet: str
- :raises RuntimeError: if the parquet is not defined in the data
- specification file or it does not have the path specified.
- :returns: Path.
- :rtype: str
- """
-
- try:
- return self._data_spec[parquet]["path"]
- except KeyError as err:
- raise RuntimeError(
- f"The parquet {parquet} is not defined in the specification "
- f"file {self._data_spec_file} or it does not have the path "
- f"specified.\n{err}"
- )
-
- def _get_list_of_files(self,
- path,
- last_modified_begin=None,
- last_modified_end=None,
- days=None) -> list:
- """Get list of interested files stored in S3 compatible storage and
- returns it.
-
- :param path: S3 prefix (accepts Unix shell-style wildcards)
- (e.g. s3://bucket/prefix) or list of S3 objects paths
- (e.g. [s3://bucket/key0, s3://bucket/key1]).
- :param last_modified_begin: Filter the s3 files by the Last modified
- date of the object. The filter is applied only after list all s3
- files.
- :param last_modified_end: Filter the s3 files by the Last modified date
- of the object. The filter is applied only after list all s3 files.
- :param days: Number of days to filter.
- :type path: Union[str, List[str]]
- :type last_modified_begin: datetime, optional
- :type last_modified_end: datetime, optional
- :type days: integer, optional
- :returns: List of file names.
- :rtype: List
- """
- if days:
- last_modified_begin = datetime.now(tz=UTC) - timedelta(days=days)
- try:
- file_list = wr.s3.list_objects(
- path=path,
- suffix="parquet",
- last_modified_begin=last_modified_begin,
- last_modified_end=last_modified_end
- )
- if self._debug:
- logging.info("\n".join(file_list))
- except NoFilesFound as err:
- logging.error(f"No parquets found.\n{err}")
- except EmptyDataFrame as err:
- logging.error(f"No data.\n{err}")
-
- return file_list
-
- def _create_dataframe_from_parquet(self,
- path, partition_filter=None,
- columns=None,
- validate_schema=False,
- last_modified_begin=None,
- last_modified_end=None,
- days=None) -> DataFrame:
- """Read parquet stored in S3 compatible storage and returns Pandas
- Dataframe.
-
- :param path: S3 prefix (accepts Unix shell-style wildcards)
- (e.g. s3://bucket/prefix) or list of S3 objects paths
- (e.g. [s3://bucket/key0, s3://bucket/key1]).
- :param partition_filter: Callback Function filters to apply on PARTITION
- columns (PUSH-DOWN filter). This function MUST receive a single
- argument (Dict[str, str]) where keys are partitions names and values
- are partitions values. Partitions values will be always strings
- extracted from S3. This function MUST return a bool, True to read
- the partition or False to ignore it. Ignored if dataset=False.
- :param columns: Names of columns to read from the file(s).
- :param validate_schema: Check that individual file schemas are all the
- same / compatible. Schemas within a folder prefix should all be the
- same. Disable if you have schemas that are different and want to
- disable this check.
- :param last_modified_begin: Filter the s3 files by the Last modified
- date of the object. The filter is applied only after list all s3
- files.
- :param last_modified_end: Filter the s3 files by the Last modified date
- of the object. The filter is applied only after list all s3 files.
- :param days: Number of days to filter.
- :type path: Union[str, List[str]]
- :type partition_filter: Callable[[Dict[str, str]], bool], optional
- :type columns: List[str], optional
- :type validate_schema: bool, optional
- :type last_modified_begin: datetime, optional
- :type last_modified_end: datetime, optional
- :type days: integer, optional
- :returns: Pandas DataFrame or None if DataFrame cannot be fetched.
- :rtype: DataFrame
- """
- df = None
- start = time()
- if days:
- last_modified_begin = datetime.now(tz=UTC) - timedelta(days=days)
- try:
- df = wr.s3.read_parquet(
- path=path,
- path_suffix="parquet",
- ignore_empty=True,
- validate_schema=validate_schema,
- use_threads=True,
- dataset=True,
- columns=columns,
- partition_filter=partition_filter,
- last_modified_begin=last_modified_begin,
- last_modified_end=last_modified_end
- )
- if self._debug:
- df.info(verbose=True, memory_usage='deep')
- logging.info(
- u"\n"
- f"Creation of dataframe {path} took: {time() - start}"
- u"\n"
- )
- except NoFilesFound as err:
- logging.error(f"No parquets found.\n{err}")
- except EmptyDataFrame as err:
- logging.error(f"No data.\n{err}")
-
- self._data = df
- return df
-
- def check_datasets(self, days: int=None):
- """Read structure from parquet.
-
- :param days: Number of days back to the past for which the data will be
- read.
- :type days: int
- """
- self._get_list_of_files(path=self._get_path("trending"), days=days)
- self._get_list_of_files(path=self._get_path("statistics"), days=days)
-
- def read_stats(self, days: int=None) -> tuple:
- """Read statistics from parquet.
-
- It reads from:
- - Suite Result Analysis (SRA) partition,
- - NDRPDR trending partition,
- - MRR trending partition.
-
- :param days: Number of days back to the past for which the data will be
- read.
- :type days: int
- :returns: tuple of pandas DataFrame-s with data read from specified
- parquets.
- :rtype: tuple of pandas DataFrame-s
- """
-
- l_stats = lambda part: True if part["stats_type"] == "sra" else False
- l_mrr = lambda part: True if part["test_type"] == "mrr" else False
- l_ndrpdr = lambda part: True if part["test_type"] == "ndrpdr" else False
-
- return (
- self._create_dataframe_from_parquet(
- path=self._get_path("statistics"),
- partition_filter=l_stats,
- columns=self._get_columns("statistics"),
- days=days
- ),
- self._create_dataframe_from_parquet(
- path=self._get_path("statistics-trending-mrr"),
- partition_filter=l_mrr,
- columns=self._get_columns("statistics-trending-mrr"),
- days=days
- ),
- self._create_dataframe_from_parquet(
- path=self._get_path("statistics-trending-ndrpdr"),
- partition_filter=l_ndrpdr,
- columns=self._get_columns("statistics-trending-ndrpdr"),
- days=days
- )
- )
-
- def read_trending_mrr(self, days: int=None) -> DataFrame:
- """Read MRR data partition from parquet.
-
- :param days: Number of days back to the past for which the data will be
- read.
- :type days: int
- :returns: Pandas DataFrame with read data.
- :rtype: DataFrame
- """
-
- lambda_f = lambda part: True if part["test_type"] == "mrr" else False
-
- return self._create_dataframe_from_parquet(
- path=self._get_path("trending-mrr"),
- partition_filter=lambda_f,
- columns=self._get_columns("trending-mrr"),
- days=days
- )
-
- def read_trending_ndrpdr(self, days: int=None) -> DataFrame:
- """Read NDRPDR data partition from iterative parquet.
-
- :param days: Number of days back to the past for which the data will be
- read.
- :type days: int
- :returns: Pandas DataFrame with read data.
- :rtype: DataFrame
- """
-
- lambda_f = lambda part: True if part["test_type"] == "ndrpdr" else False
-
- return self._create_dataframe_from_parquet(
- path=self._get_path("trending-ndrpdr"),
- partition_filter=lambda_f,
- columns=self._get_columns("trending-ndrpdr"),
- days=days
- )
-
- def read_iterative_mrr(self, release: str) -> DataFrame:
- """Read MRR data partition from iterative parquet.
-
- :param release: The CSIT release from which the data will be read.
- :type release: str
- :returns: Pandas DataFrame with read data.
- :rtype: DataFrame
- """
-
- lambda_f = lambda part: True if part["test_type"] == "mrr" else False
-
- return self._create_dataframe_from_parquet(
- path=self._get_path("iterative-mrr").format(release=release),
- partition_filter=lambda_f,
- columns=self._get_columns("iterative-mrr")
- )
-
- def read_iterative_ndrpdr(self, release: str) -> DataFrame:
- """Read NDRPDR data partition from parquet.
-
- :param release: The CSIT release from which the data will be read.
- :type release: str
- :returns: Pandas DataFrame with read data.
- :rtype: DataFrame
- """
-
- lambda_f = lambda part: True if part["test_type"] == "ndrpdr" else False
-
- return self._create_dataframe_from_parquet(
- path=self._get_path("iterative-ndrpdr").format(release=release),
- partition_filter=lambda_f,
- columns=self._get_columns("iterative-ndrpdr")
- )
diff --git a/resources/tools/dash/app/pal/data/data.yaml b/resources/tools/dash/app/pal/data/data.yaml
deleted file mode 100644
index 396f1b1638..0000000000
--- a/resources/tools/dash/app/pal/data/data.yaml
+++ /dev/null
@@ -1,117 +0,0 @@
-statistics:
- path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/stats
- columns:
- - job
- - build
- - start_time
- - duration
-statistics-trending-ndrpdr:
- path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/trending
- columns:
- - job
- - build
- - dut_type
- - dut_version
- - hosts
- - start_time
- - passed
- - test_id
- - result_pdr_lower_rate_value
- - result_ndr_lower_rate_value
-statistics-trending-mrr:
- path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/trending
- columns:
- - job
- - build
- - dut_type
- - dut_version
- - hosts
- - start_time
- - passed
- - test_id
- - result_receive_rate_rate_avg
-trending:
- path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/trending
- columns:
- - job
- - build
- - start_time
-trending-mrr:
- path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/trending
- columns:
- - job
- - build
- - dut_type
- - dut_version
- - hosts
- - start_time
- - passed
- - test_id
- - version
- - result_receive_rate_rate_avg
- - result_receive_rate_rate_stdev
- - result_receive_rate_rate_unit
-trending-ndrpdr:
- path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/trending
- columns:
- - job
- - build
- - dut_type
- - dut_version
- - hosts
- - start_time
- - passed
- - test_id
- - version
- - result_pdr_lower_rate_unit
- - result_pdr_lower_rate_value
- - result_ndr_lower_rate_unit
- - result_ndr_lower_rate_value
- - result_latency_reverse_pdr_90_hdrh
- - result_latency_reverse_pdr_50_hdrh
- - result_latency_reverse_pdr_10_hdrh
- - result_latency_reverse_pdr_0_hdrh
- - result_latency_forward_pdr_90_hdrh
- - result_latency_forward_pdr_50_avg
- - result_latency_forward_pdr_50_hdrh
- - result_latency_forward_pdr_50_unit
- - result_latency_forward_pdr_10_hdrh
- - result_latency_forward_pdr_0_hdrh
-iterative-mrr:
- path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/iterative_{release}
- columns:
- - job
- - build
- - dut_type
- - dut_version
- - hosts
- - start_time
- - passed
- - test_id
- - version
- - result_receive_rate_rate_avg
- - result_receive_rate_rate_stdev
- - result_receive_rate_rate_unit
- - result_receive_rate_rate_values
-iterative-ndrpdr:
- path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/iterative_{release}
- columns:
- - job
- - build
- - dut_type
- - dut_version
- - hosts
- - start_time
- - passed
- - test_id
- - version
- - result_pdr_lower_rate_unit
- - result_pdr_lower_rate_value
- - result_ndr_lower_rate_unit
- - result_ndr_lower_rate_value
- - result_latency_forward_pdr_50_avg
- - result_latency_forward_pdr_50_unit
-# coverage-ndrpdr:
-# path: str
-# columns:
-# - list
diff --git a/resources/tools/dash/app/pal/debug.py b/resources/tools/dash/app/pal/debug.py
deleted file mode 100644
index f0543820b1..0000000000
--- a/resources/tools/dash/app/pal/debug.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import logging
-
-from data.data import Data
-from utils.constants import Constants as C
-
-
-logging.basicConfig(
- format=u"%(asctime)s: %(levelname)s: %(message)s",
- datefmt=u"%Y/%m/%d %H:%M:%S",
- level=logging.INFO
-)
-
-# Set the time period for data fetch
-if C.TIME_PERIOD is None or C.TIME_PERIOD > C.MAX_TIME_PERIOD:
- time_period = C.MAX_TIME_PERIOD
-else:
- time_period = C.TIME_PERIOD
-
-#data_mrr = Data(
-# data_spec_file=C.DATA_SPEC_FILE,
-# debug=True
-#).read_trending_mrr(days=time_period)
-#
-#data_ndrpdr = Data(
-# data_spec_file=C.DATA_SPEC_FILE,
-# debug=True
-#).read_trending_ndrpdr(days=time_period)
-
-data_list = Data(
- data_spec_file=C.DATA_SPEC_FILE,
- debug=True
-).check_datasets(days=time_period) \ No newline at end of file
diff --git a/resources/tools/dash/app/pal/news/__init__.py b/resources/tools/dash/app/pal/news/__init__.py
deleted file mode 100644
index 5692432123..0000000000
--- a/resources/tools/dash/app/pal/news/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
diff --git a/resources/tools/dash/app/pal/news/layout.py b/resources/tools/dash/app/pal/news/layout.py
deleted file mode 100644
index 73fabdf884..0000000000
--- a/resources/tools/dash/app/pal/news/layout.py
+++ /dev/null
@@ -1,707 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Plotly Dash HTML layout override.
-"""
-
-import logging
-import pandas as pd
-import dash_bootstrap_components as dbc
-
-from flask import Flask
-from dash import dcc
-from dash import html
-from dash import callback_context
-from dash import Input, Output, State
-from yaml import load, FullLoader, YAMLError
-from copy import deepcopy
-
-from ..data.data import Data
-from ..utils.constants import Constants as C
-from ..utils.utils import classify_anomalies, show_tooltip, gen_new_url, \
- get_ttypes, get_cadences, get_test_beds, get_job, generate_options, \
- set_job_params
-from ..utils.url_processing import url_decode
-from ..data.data import Data
-from .tables import table_news
-
-
-class Layout:
- """The layout of the dash app and the callbacks.
- """
-
- def __init__(self, app: Flask, html_layout_file: str, data_spec_file: str,
- tooltip_file: str) -> None:
- """Initialization:
- - save the input parameters,
- - read and pre-process the data,
- - prepare data for the control panel,
- - read HTML layout file,
- - read tooltips from the tooltip file.
-
- :param app: Flask application running the dash application.
- :param html_layout_file: Path and name of the file specifying the HTML
- layout of the dash application.
- :param data_spec_file: Path and name of the file specifying the data to
- be read from parquets for this application.
- :param tooltip_file: Path and name of the yaml file specifying the
- tooltips.
- :type app: Flask
- :type html_layout_file: str
- :type data_spec_file: str
- :type tooltip_file: str
- """
-
- # Inputs
- self._app = app
- self._html_layout_file = html_layout_file
- self._data_spec_file = data_spec_file
- self._tooltip_file = tooltip_file
-
- # Read the data:
- data_stats, data_mrr, data_ndrpdr = Data(
- data_spec_file=self._data_spec_file,
- debug=True
- ).read_stats(days=C.NEWS_TIME_PERIOD)
-
- df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
-
- # Prepare information for the control panel:
- jobs = sorted(list(df_tst_info["job"].unique()))
- d_job_info = {
- "job": list(),
- "dut": list(),
- "ttype": list(),
- "cadence": list(),
- "tbed": list()
- }
- for job in jobs:
- lst_job = job.split("-")
- d_job_info["job"].append(job)
- d_job_info["dut"].append(lst_job[1])
- d_job_info["ttype"].append(lst_job[3])
- d_job_info["cadence"].append(lst_job[4])
- d_job_info["tbed"].append("-".join(lst_job[-2:]))
- self.job_info = pd.DataFrame.from_dict(d_job_info)
-
- self._default = set_job_params(self.job_info, C.NEWS_DEFAULT_JOB)
-
- # Pre-process the data:
-
- def _create_test_name(test: str) -> str:
- lst_tst = test.split(".")
- suite = lst_tst[-2].replace("2n1l-", "").replace("1n1l-", "").\
- replace("2n-", "")
- return f"{suite.split('-')[0]}-{lst_tst[-1]}"
-
- def _get_rindex(array: list, itm: any) -> int:
- return len(array) - 1 - array[::-1].index(itm)
-
- tst_info = {
- "job": list(),
- "build": list(),
- "start": list(),
- "dut_type": list(),
- "dut_version": list(),
- "hosts": list(),
- "failed": list(),
- "regressions": list(),
- "progressions": list()
- }
- for job in jobs:
- # Create lists of failed tests:
- df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
- last_build = max(df_job["build"].unique())
- df_build = df_job.loc[(df_job["build"] == last_build)]
- tst_info["job"].append(job)
- tst_info["build"].append(last_build)
- tst_info["start"].append(data_stats.loc[
- (data_stats["job"] == job) &
- (data_stats["build"] == last_build)
- ]["start_time"].iloc[-1].strftime('%Y-%m-%d %H:%M'))
- tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
- tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
- tst_info["hosts"].append(df_build["hosts"].iloc[-1])
- failed_tests = df_build.loc[(df_build["passed"] == False)]\
- ["test_id"].to_list()
- l_failed = list()
- try:
- for tst in failed_tests:
- l_failed.append(_create_test_name(tst))
- except KeyError:
- l_failed = list()
- tst_info["failed"].append(sorted(l_failed))
-
- # Create lists of regressions and progressions:
- l_reg = list()
- l_prog = list()
-
- tests = df_job["test_id"].unique()
- for test in tests:
- tst_data = df_job.loc[df_job["test_id"] == test].sort_values(
- by="start_time", ignore_index=True)
- x_axis = tst_data["start_time"].tolist()
- if "-ndrpdr" in test:
- tst_data = tst_data.dropna(
- subset=["result_pdr_lower_rate_value", ]
- )
- if tst_data.empty:
- continue
- try:
- anomalies, _, _ = classify_anomalies({
- k: v for k, v in zip(
- x_axis,
- tst_data["result_ndr_lower_rate_value"].tolist()
- )
- })
- except ValueError:
- continue
- if "progression" in anomalies:
- l_prog.append((
- _create_test_name(test).replace("-ndrpdr", "-ndr"),
- x_axis[_get_rindex(anomalies, "progression")]
- ))
- if "regression" in anomalies:
- l_reg.append((
- _create_test_name(test).replace("-ndrpdr", "-ndr"),
- x_axis[_get_rindex(anomalies, "regression")]
- ))
- try:
- anomalies, _, _ = classify_anomalies({
- k: v for k, v in zip(
- x_axis,
- tst_data["result_pdr_lower_rate_value"].tolist()
- )
- })
- except ValueError:
- continue
- if "progression" in anomalies:
- l_prog.append((
- _create_test_name(test).replace("-ndrpdr", "-pdr"),
- x_axis[_get_rindex(anomalies, "progression")]
- ))
- if "regression" in anomalies:
- l_reg.append((
- _create_test_name(test).replace("-ndrpdr", "-pdr"),
- x_axis[_get_rindex(anomalies, "regression")]
- ))
- else: # mrr
- tst_data = tst_data.dropna(
- subset=["result_receive_rate_rate_avg", ]
- )
- if tst_data.empty:
- continue
- try:
- anomalies, _, _ = classify_anomalies({
- k: v for k, v in zip(
- x_axis,
- tst_data["result_receive_rate_rate_avg"].\
- tolist()
- )
- })
- except ValueError:
- continue
- if "progression" in anomalies:
- l_prog.append((
- _create_test_name(test),
- x_axis[_get_rindex(anomalies, "progression")]
- ))
- if "regression" in anomalies:
- l_reg.append((
- _create_test_name(test),
- x_axis[_get_rindex(anomalies, "regression")]
- ))
-
- tst_info["regressions"].append(
- sorted(l_reg, key=lambda k: k[1], reverse=True))
- tst_info["progressions"].append(
- sorted(l_prog, key=lambda k: k[1], reverse=True))
-
- self._data = pd.DataFrame.from_dict(tst_info)
-
- # Read from files:
- self._html_layout = str()
- self._tooltips = dict()
-
- try:
- with open(self._html_layout_file, "r") as file_read:
- self._html_layout = file_read.read()
- except IOError as err:
- raise RuntimeError(
- f"Not possible to open the file {self._html_layout_file}\n{err}"
- )
-
- try:
- with open(self._tooltip_file, "r") as file_read:
- self._tooltips = load(file_read, Loader=FullLoader)
- except IOError as err:
- logging.warning(
- f"Not possible to open the file {self._tooltip_file}\n{err}"
- )
- except YAMLError as err:
- logging.warning(
- f"An error occurred while parsing the specification file "
- f"{self._tooltip_file}\n{err}"
- )
-
- self._default_tab_failed = table_news(self.data, self._default["job"])
-
- # Callbacks:
- if self._app is not None and hasattr(self, 'callbacks'):
- self.callbacks(self._app)
-
- @property
- def html_layout(self) -> dict:
- return self._html_layout
-
- @property
- def data(self) -> pd.DataFrame:
- return self._data
-
- @property
- def default(self) -> dict:
- return self._default
-
- def add_content(self):
- """Top level method which generated the web page.
-
- It generates:
- - Store for user input data,
- - Navigation bar,
- - Main area with control panel and ploting area.
-
- If no HTML layout is provided, an error message is displayed instead.
-
- :returns: The HTML div with the whole page.
- :rtype: html.Div
- """
-
- if self.html_layout:
- return html.Div(
- id="div-main",
- children=[
- dcc.Store(id="control-panel"),
- dcc.Location(id="url", refresh=False),
- dbc.Row(
- id="row-navbar",
- class_name="g-0",
- children=[
- self._add_navbar(),
- ]
- ),
- dbc.Row(
- id="row-main",
- class_name="g-0",
- children=[
- self._add_ctrl_col(),
- self._add_plotting_col(),
- ]
- )
- ]
- )
- else:
- return html.Div(
- id="div-main-error",
- children=[
- dbc.Alert(
- [
- "An Error Occured",
- ],
- color="danger",
- ),
- ]
- )
-
- def _add_navbar(self):
- """Add nav element with navigation panel. It is placed on the top.
-
- :returns: Navigation bar.
- :rtype: dbc.NavbarSimple
- """
-
- return dbc.NavbarSimple(
- id="navbarsimple-main",
- children=[
- dbc.NavItem(
- dbc.NavLink(
- "Continuous Performance News",
- disabled=True,
- external_link=True,
- href="#"
- )
- )
- ],
- brand="Dashboard",
- brand_href="/",
- brand_external_link=True,
- class_name="p-2",
- fluid=True,
- )
-
- def _add_ctrl_col(self) -> dbc.Col:
- """Add column with control panel. It is placed on the left side.
-
- :returns: Column with the control panel.
- :rtype: dbc.Col
- """
-
- return dbc.Col(
- id="col-controls",
- children=[
- self._add_ctrl_panel(),
- ],
- )
-
- def _add_plotting_col(self) -> dbc.Col:
- """Add column with tables. It is placed on the right side.
-
- :returns: Column with tables.
- :rtype: dbc.Col
- """
-
- return dbc.Col(
- id="col-plotting-area",
- children=[
- dbc.Row( # Failed tests
- id="row-table-failed",
- class_name="g-0 p-2",
- children=self._default_tab_failed
- ),
- dbc.Row(
- class_name="g-0 p-2",
- align="center",
- justify="start",
- children=[
- dbc.InputGroup(
- class_name="me-1",
- children=[
- dbc.InputGroupText(
- style=C.URL_STYLE,
- children=show_tooltip(
- self._tooltips,
- "help-url", "URL",
- "input-url"
- )
- ),
- dbc.Input(
- id="input-url",
- readonly=True,
- type="url",
- style=C.URL_STYLE,
- value=""
- )
- ]
- )
- ]
- )
- ],
- width=9,
- )
-
- def _add_ctrl_panel(self) -> dbc.Row:
- """Add control panel.
-
- :returns: Control panel.
- :rtype: dbc.Row
- """
- return dbc.Row(
- id="row-ctrl-panel",
- class_name="g-0",
- children=[
- dbc.Row(
- class_name="g-0 p-2",
- children=[
- dbc.Row(
- class_name="gy-1",
- children=[
- dbc.Label(
- class_name="p-0",
- children=show_tooltip(self._tooltips,
- "help-dut", "Device under Test")
- ),
- dbc.Row(
- dbc.RadioItems(
- id="ri-duts",
- inline=True,
- value=self.default["dut"],
- options=self.default["duts"]
- )
- )
- ]
- ),
- dbc.Row(
- class_name="gy-1",
- children=[
- dbc.Label(
- class_name="p-0",
- children=show_tooltip(self._tooltips,
- "help-ttype", "Test Type"),
- ),
- dbc.RadioItems(
- id="ri-ttypes",
- inline=True,
- value=self.default["ttype"],
- options=self.default["ttypes"]
- )
- ]
- ),
- dbc.Row(
- class_name="gy-1",
- children=[
- dbc.Label(
- class_name="p-0",
- children=show_tooltip(self._tooltips,
- "help-cadence", "Cadence"),
- ),
- dbc.RadioItems(
- id="ri-cadences",
- inline=True,
- value=self.default["cadence"],
- options=self.default["cadences"]
- )
- ]
- ),
- dbc.Row(
- class_name="gy-1",
- children=[
- dbc.Label(
- class_name="p-0",
- children=show_tooltip(self._tooltips,
- "help-tbed", "Test Bed"),
- ),
- dbc.Select(
- id="dd-tbeds",
- placeholder="Select a test bed...",
- value=self.default["tbed"],
- options=self.default["tbeds"]
- )
- ]
- ),
- dbc.Row(
- class_name="gy-1",
- children=[
- dbc.Alert(
- id="al-job",
- color="info",
- children=self.default["job"]
- )
- ]
- )
- ]
- )
- ]
- )
-
- class ControlPanel:
- """A class representing the control panel.
- """
-
- def __init__(self, panel: dict, default: dict) -> None:
- """Initialisation of the control pannel by default values. If
- particular values are provided (parameter "panel") they are set
- afterwards.
-
- :param panel: Custom values to be set to the control panel.
- :param default: Default values to be set to the control panel.
- :type panel: dict
- :type defaults: dict
- """
-
- self._defaults = {
- "ri-ttypes-options": default["ttypes"],
- "ri-cadences-options": default["cadences"],
- "dd-tbeds-options": default["tbeds"],
- "ri-duts-value": default["dut"],
- "ri-ttypes-value": default["ttype"],
- "ri-cadences-value": default["cadence"],
- "dd-tbeds-value": default["tbed"],
- "al-job-children": default["job"]
- }
- self._panel = deepcopy(self._defaults)
- if panel:
- for key in self._defaults:
- self._panel[key] = panel[key]
-
- def set(self, kwargs: dict) -> None:
- """Set the values of the Control panel.
-
- :param kwargs: key - value pairs to be set.
- :type kwargs: dict
- :raises KeyError: If the key in kwargs is not present in the Control
- panel.
- """
- for key, val in kwargs.items():
- if key in self._panel:
- self._panel[key] = val
- else:
- raise KeyError(f"The key {key} is not defined.")
-
- @property
- def defaults(self) -> dict:
- return self._defaults
-
- @property
- def panel(self) -> dict:
- return self._panel
-
- def get(self, key: str) -> any:
- """Returns the value of a key from the Control panel.
-
- :param key: The key which value should be returned.
- :type key: str
- :returns: The value of the key.
- :rtype: any
- :raises KeyError: If the key in kwargs is not present in the Control
- panel.
- """
- return self._panel[key]
-
- def values(self) -> list:
- """Returns the values from the Control panel as a list.
-
- :returns: The values from the Control panel.
- :rtype: list
- """
- return list(self._panel.values())
-
- def callbacks(self, app):
- """Callbacks for the whole application.
-
- :param app: The application.
- :type app: Flask
- """
-
- @app.callback(
- Output("control-panel", "data"), # Store
- Output("row-table-failed", "children"),
- Output("input-url", "value"),
- Output("ri-ttypes", "options"),
- Output("ri-cadences", "options"),
- Output("dd-tbeds", "options"),
- Output("ri-duts", "value"),
- Output("ri-ttypes", "value"),
- Output("ri-cadences", "value"),
- Output("dd-tbeds", "value"),
- Output("al-job", "children"),
- State("control-panel", "data"), # Store
- Input("ri-duts", "value"),
- Input("ri-ttypes", "value"),
- Input("ri-cadences", "value"),
- Input("dd-tbeds", "value"),
- Input("url", "href")
- )
- def _update_application(cp_data: dict, dut: str, ttype: str,
- cadence:str, tbed: str, href: str) -> tuple:
- """Update the application when the event is detected.
-
- :param cp_data: Current status of the control panel stored in
- browser.
- :param dut: Input - DUT name.
- :param ttype: Input - Test type.
- :param cadence: Input - The cadence of the job.
- :param tbed: Input - The test bed.
- :param href: Input - The URL provided by the browser.
- :type cp_data: dict
- :type dut: str
- :type ttype: str
- :type cadence: str
- :type tbed: str
- :type href: str
- :returns: New values for web page elements.
- :rtype: tuple
- """
-
- ctrl_panel = self.ControlPanel(cp_data, self.default)
-
- # Parse the url:
- parsed_url = url_decode(href)
- if parsed_url:
- url_params = parsed_url["params"]
- else:
- url_params = None
-
- trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
- if trigger_id == "ri-duts":
- ttype_opts = generate_options(get_ttypes(self.job_info, dut))
- ttype_val = ttype_opts[0]["value"]
- cad_opts = generate_options(
- get_cadences(self.job_info, dut, ttype_val))
- cad_val = cad_opts[0]["value"]
- tbed_opts = generate_options(get_test_beds(
- self.job_info, dut, ttype_val, cad_val))
- tbed_val = tbed_opts[0]["value"]
- ctrl_panel.set({
- "ri-duts-value": dut,
- "ri-ttypes-options": ttype_opts,
- "ri-ttypes-value": ttype_val,
- "ri-cadences-options": cad_opts,
- "ri-cadences-value": cad_val,
- "dd-tbeds-options": tbed_opts,
- "dd-tbeds-value": tbed_val
- })
- elif trigger_id == "ri-ttypes":
- cad_opts = generate_options(get_cadences(
- self.job_info, ctrl_panel.get("ri-duts-value"), ttype))
- cad_val = cad_opts[0]["value"]
- tbed_opts = generate_options(get_test_beds(
- self.job_info, ctrl_panel.get("ri-duts-value"),
- ttype, cad_val))
- tbed_val = tbed_opts[0]["value"]
- ctrl_panel.set({
- "ri-ttypes-value": ttype,
- "ri-cadences-options": cad_opts,
- "ri-cadences-value": cad_val,
- "dd-tbeds-options": tbed_opts,
- "dd-tbeds-value": tbed_val
- })
- elif trigger_id == "ri-cadences":
- tbed_opts = generate_options(get_test_beds(
- self.job_info, ctrl_panel.get("ri-duts-value"),
- ctrl_panel.get("ri-ttypes-value"), cadence))
- tbed_val = tbed_opts[0]["value"]
- ctrl_panel.set({
- "ri-cadences-value": cadence,
- "dd-tbeds-options": tbed_opts,
- "dd-tbeds-value": tbed_val
- })
- elif trigger_id == "dd-tbeds":
- ctrl_panel.set({
- "dd-tbeds-value": tbed
- })
- elif trigger_id == "url":
- # TODO: Add verification
- if url_params:
- new_job = url_params.get("job", list())[0]
- if new_job:
- job_params = set_job_params(self.job_info, new_job)
- ctrl_panel = self.ControlPanel(None, job_params)
- else:
- ctrl_panel = self.ControlPanel(cp_data, self.default)
-
- job = get_job(
- self.job_info,
- ctrl_panel.get("ri-duts-value"),
- ctrl_panel.get("ri-ttypes-value"),
- ctrl_panel.get("ri-cadences-value"),
- ctrl_panel.get("dd-tbeds-value")
- )
- ctrl_panel.set({"al-job-children": job})
- tab_failed = table_news(self.data, job)
-
- ret_val = [
- ctrl_panel.panel,
- tab_failed,
- gen_new_url(parsed_url, {"job": job})
- ]
- ret_val.extend(ctrl_panel.values())
- return ret_val
diff --git a/resources/tools/dash/app/pal/news/news.py b/resources/tools/dash/app/pal/news/news.py
deleted file mode 100644
index a0d05f1483..0000000000
--- a/resources/tools/dash/app/pal/news/news.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Instantiate the News Dash application.
-"""
-import dash
-
-from ..utils.constants import Constants as C
-from .layout import Layout
-
-
-def init_news(server):
- """Create a Plotly Dash dashboard.
-
- :param server: Flask server.
- :type server: Flask
- :returns: Dash app server.
- :rtype: Dash
- """
-
- dash_app = dash.Dash(
- server=server,
- routes_pathname_prefix=C.NEWS_ROUTES_PATHNAME_PREFIX,
- external_stylesheets=C.EXTERNAL_STYLESHEETS
- )
-
- layout = Layout(
- app=dash_app,
- html_layout_file=C.NEWS_HTML_LAYOUT_FILE,
- data_spec_file=C.DATA_SPEC_FILE,
- tooltip_file=C.TOOLTIP_FILE,
- )
- dash_app.index_string = layout.html_layout
- dash_app.layout = layout.add_content()
-
- return dash_app.server
diff --git a/resources/tools/dash/app/pal/news/tables.py b/resources/tools/dash/app/pal/news/tables.py
deleted file mode 100644
index 1a6c7d2556..0000000000
--- a/resources/tools/dash/app/pal/news/tables.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""The tables with news.
-"""
-
-import pandas as pd
-import dash_bootstrap_components as dbc
-
-from ..utils.constants import Constants as C
-
-
-def table_news(data: pd.DataFrame, job: str) -> list:
- """Generates the tables with news:
- 1. Falied tests from the last run
- 2. Regressions and progressions calculated from the last C.NEWS_TIME_PERIOD
- days.
-
- :param data: Trending data with calculated annomalies to be displayed in the
- tables.
- :param job: The job name.
- :type data: pandas.DataFrame
- :type job: str
- """
-
- job_data = data.loc[(data["job"] == job)]
- failed = job_data["failed"].to_list()[0]
- regressions = {"Test Name": list(), "Last Regression": list()}
- for itm in job_data["regressions"].to_list()[0]:
- regressions["Test Name"].append(itm[0])
- regressions["Last Regression"].append(itm[1].strftime('%Y-%m-%d %H:%M'))
- progressions = {"Test Name": list(), "Last Progression": list()}
- for itm in job_data["progressions"].to_list()[0]:
- progressions["Test Name"].append(itm[0])
- progressions["Last Progression"].append(
- itm[1].strftime('%Y-%m-%d %H:%M'))
-
- return [
- dbc.Table.from_dataframe(pd.DataFrame.from_dict({
- "Job": job_data["job"],
- "Last Build": job_data["build"],
- "Date": job_data["start"],
- "DUT": job_data["dut_type"],
- "DUT Version": job_data["dut_version"],
- "Hosts": ", ".join(job_data["hosts"].to_list()[0])
- }), bordered=True, striped=True, hover=True, size="sm", color="light"),
- dbc.Table.from_dataframe(pd.DataFrame.from_dict({
- (
- f"Last Failed Tests on "
- f"{job_data['start'].values[0]} ({len(failed)})"
- ): failed
- }), bordered=True, striped=True, hover=True, size="sm", color="light"),
- dbc.Label(
- class_name="p-0",
- size="lg",
- children=(
- f"Regressions during the last {C.NEWS_TIME_PERIOD} days "
- f"({len(regressions['Test Name'])})"
- )
- ),
- dbc.Table.from_dataframe(
- pd.DataFrame.from_dict(regressions),
- bordered=True, striped=True, hover=True, size="sm", color="light"),
- dbc.Label(
- class_name="p-0",
- size="lg",
- children=(
- f"Progressions during the last {C.NEWS_TIME_PERIOD} days "
- f"({len(progressions['Test Name'])})"
- )
- ),
- dbc.Table.from_dataframe(
- pd.DataFrame.from_dict(progressions),
- bordered=True, striped=True, hover=True, size="sm", color="light")
- ]
diff --git a/resources/tools/dash/app/pal/report/__init__.py b/resources/tools/dash/app/pal/report/__init__.py
deleted file mode 100644
index 5692432123..0000000000
--- a/resources/tools/dash/app/pal/report/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
diff --git a/resources/tools/dash/app/pal/report/graphs.py b/resources/tools/dash/app/pal/report/graphs.py
deleted file mode 100644
index 36f28d09e8..0000000000
--- a/resources/tools/dash/app/pal/report/graphs.py
+++ /dev/null
@@ -1,275 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""
-"""
-
-import re
-import plotly.graph_objects as go
-import pandas as pd
-
-from copy import deepcopy
-
-from ..utils.constants import Constants as C
-from ..utils.utils import get_color
-
-
-def get_short_version(version: str, dut_type: str="vpp") -> str:
- """Returns the short version of DUT without build number.
-
- :param version: Original version string.
- :param dut_type: DUT type.
- :type version: str
- :type dut_type: str
- :returns: Short verion string.
- :rtype: str
- """
-
- if dut_type in ("trex", "dpdk"):
- return version
-
- s_version = str()
- groups = re.search(
- pattern=re.compile(r"^(\d{2}).(\d{2})-(rc0|rc1|rc2|release$)"),
- string=version
- )
- if groups:
- try:
- s_version = \
- f"{groups.group(1)}.{groups.group(2)}.{groups.group(3)}".\
- replace("release", "rls")
- except IndexError:
- pass
-
- return s_version
-
-
-def select_iterative_data(data: pd.DataFrame, itm:dict) -> pd.DataFrame:
- """Select the data for graphs and tables from the provided data frame.
-
- :param data: Data frame with data for graphs and tables.
- :param itm: Item (in this case job name) which data will be selected from
- the input data frame.
- :type data: pandas.DataFrame
- :type itm: str
- :returns: A data frame with selected data.
- :rtype: pandas.DataFrame
- """
-
- phy = itm["phy"].split("-")
- if len(phy) == 4:
- topo, arch, nic, drv = phy
- if drv == "dpdk":
- drv = ""
- else:
- drv += "-"
- drv = drv.replace("_", "-")
- else:
- return None
-
- core = str() if itm["dut"] == "trex" else f"{itm['core']}"
- ttype = "ndrpdr" if itm["testtype"] in ("ndr", "pdr") else itm["testtype"]
- dut_v100 = "none" if itm["dut"] == "trex" else itm["dut"]
- dut_v101 = itm["dut"]
-
- df = data.loc[(
- (data["release"] == itm["rls"]) &
- (
- (
- (data["version"] == "1.0.0") &
- (data["dut_type"].str.lower() == dut_v100)
- ) |
- (
- (data["version"] == "1.0.1") &
- (data["dut_type"].str.lower() == dut_v101)
- )
- ) &
- (data["test_type"] == ttype) &
- (data["passed"] == True)
- )]
- regex_test = \
- f"^.*[.|-]{nic}.*{itm['framesize']}-{core}-{drv}{itm['test']}-{ttype}$"
- df = df[
- (df.job.str.endswith(f"{topo}-{arch}")) &
- (df.dut_version.str.contains(itm["dutver"].replace(".r", "-r").\
- replace("rls", "release"))) &
- (df.test_id.str.contains(regex_test, regex=True))
- ]
-
- return df
-
-
-def graph_iterative(data: pd.DataFrame, sel:dict, layout: dict,
- normalize: bool) -> tuple:
- """Generate the statistical box graph with iterative data (MRR, NDR and PDR,
- for PDR also Latencies).
-
- :param data: Data frame with iterative data.
- :param sel: Selected tests.
- :param layout: Layout of plot.ly graph.
- :param normalize: If True, the data is normalized to CPU frquency
- Constants.NORM_FREQUENCY.
- :param data: pandas.DataFrame
- :param sel: dict
- :param layout: dict
- :param normalize: bool
- :returns: Tuple of graphs - throughput and latency.
- :rtype: tuple(plotly.graph_objects.Figure, plotly.graph_objects.Figure)
- """
-
- fig_tput = None
- fig_lat = None
-
- tput_traces = list()
- y_tput_max = 0
- lat_traces = list()
- y_lat_max = 0
- x_lat = list()
- show_latency = False
- show_tput = False
- for idx, itm in enumerate(sel):
- itm_data = select_iterative_data(data, itm)
- if itm_data.empty:
- continue
- phy = itm["phy"].split("-")
- topo_arch = f"{phy[0]}-{phy[1]}" if len(phy) == 4 else str()
- norm_factor = (C.NORM_FREQUENCY / C.FREQUENCY[topo_arch]) \
- if normalize else 1.0
- if itm["testtype"] == "mrr":
- y_data_raw = itm_data[C.VALUE_ITER[itm["testtype"]]].to_list()[0]
- y_data = [(y * norm_factor) for y in y_data_raw]
- if len(y_data) > 0:
- y_tput_max = \
- max(y_data) if max(y_data) > y_tput_max else y_tput_max
- else:
- y_data_raw = itm_data[C.VALUE_ITER[itm["testtype"]]].to_list()
- y_data = [(y * norm_factor) for y in y_data_raw]
- if y_data:
- y_tput_max = \
- max(y_data) if max(y_data) > y_tput_max else y_tput_max
- nr_of_samples = len(y_data)
- tput_kwargs = dict(
- y=y_data,
- name=(
- f"{idx + 1}. "
- f"({nr_of_samples:02d} "
- f"run{'s' if nr_of_samples > 1 else ''}) "
- f"{itm['id']}"
- ),
- hoverinfo=u"y+name",
- boxpoints="all",
- jitter=0.3,
- marker=dict(color=get_color(idx))
- )
- tput_traces.append(go.Box(**tput_kwargs))
- show_tput = True
-
- if itm["testtype"] == "pdr":
- y_lat_row = itm_data[C.VALUE_ITER["pdr-lat"]].to_list()
- y_lat = [(y / norm_factor) for y in y_lat_row]
- if y_lat:
- y_lat_max = max(y_lat) if max(y_lat) > y_lat_max else y_lat_max
- nr_of_samples = len(y_lat)
- lat_kwargs = dict(
- y=y_lat,
- name=(
- f"{idx + 1}. "
- f"({nr_of_samples:02d} "
- f"run{u's' if nr_of_samples > 1 else u''}) "
- f"{itm['id']}"
- ),
- hoverinfo="all",
- boxpoints="all",
- jitter=0.3,
- marker=dict(color=get_color(idx))
- )
- x_lat.append(idx + 1)
- lat_traces.append(go.Box(**lat_kwargs))
- show_latency = True
- else:
- lat_traces.append(go.Box())
-
- if show_tput:
- pl_tput = deepcopy(layout["plot-throughput"])
- pl_tput["xaxis"]["tickvals"] = [i for i in range(len(sel))]
- pl_tput["xaxis"]["ticktext"] = [str(i + 1) for i in range(len(sel))]
- if y_tput_max:
- pl_tput["yaxis"]["range"] = [0, (int(y_tput_max / 1e6) + 1) * 1e6]
- fig_tput = go.Figure(data=tput_traces, layout=pl_tput)
-
- if show_latency:
- pl_lat = deepcopy(layout["plot-latency"])
- pl_lat["xaxis"]["tickvals"] = [i for i in range(len(x_lat))]
- pl_lat["xaxis"]["ticktext"] = x_lat
- if y_lat_max:
- pl_lat["yaxis"]["range"] = [0, (int(y_lat_max / 10) + 1) * 10]
- fig_lat = go.Figure(data=lat_traces, layout=pl_lat)
-
- return fig_tput, fig_lat
-
-
-def table_comparison(data: pd.DataFrame, sel:dict,
- normalize: bool) -> pd.DataFrame:
- """Generate the comparison table with selected tests.
-
- :param data: Data frame with iterative data.
- :param sel: Selected tests.
- :param normalize: If True, the data is normalized to CPU frquency
- Constants.NORM_FREQUENCY.
- :param data: pandas.DataFrame
- :param sel: dict
- :param normalize: bool
- :returns: Comparison table.
- :rtype: pandas.DataFrame
- """
- table = pd.DataFrame(
- # {
- # "Test Case": [
- # "64b-2t1c-avf-eth-l2xcbase-eth-2memif-1dcr",
- # "64b-2t1c-avf-eth-l2xcbase-eth-2vhostvr1024-1vm-vppl2xc",
- # "64b-2t1c-avf-ethip4udp-ip4base-iacl50sl-10kflows",
- # "78b-2t1c-avf-ethip6-ip6scale2m-rnd "],
- # "2106.0-8": [
- # "14.45 +- 0.08",
- # "9.63 +- 0.05",
- # "9.7 +- 0.02",
- # "8.95 +- 0.06"],
- # "2110.0-8": [
- # "14.45 +- 0.08",
- # "9.63 +- 0.05",
- # "9.7 +- 0.02",
- # "8.95 +- 0.06"],
- # "2110.0-9": [
- # "14.45 +- 0.08",
- # "9.63 +- 0.05",
- # "9.7 +- 0.02",
- # "8.95 +- 0.06"],
- # "2202.0-9": [
- # "14.45 +- 0.08",
- # "9.63 +- 0.05",
- # "9.7 +- 0.02",
- # "8.95 +- 0.06"],
- # "2110.0-9 vs 2110.0-8": [
- # "-0.23 +- 0.62",
- # "-1.37 +- 1.3",
- # "+0.08 +- 0.2",
- # "-2.16 +- 0.83"],
- # "2202.0-9 vs 2110.0-9": [
- # "+6.95 +- 0.72",
- # "+5.35 +- 1.26",
- # "+4.48 +- 1.48",
- # "+4.09 +- 0.95"]
- # }
- )
-
- return table
diff --git a/resources/tools/dash/app/pal/report/layout.py b/resources/tools/dash/app/pal/report/layout.py
deleted file mode 100644
index 978ab0de6c..0000000000
--- a/resources/tools/dash/app/pal/report/layout.py
+++ /dev/null
@@ -1,1446 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Plotly Dash HTML layout override.
-"""
-
-import logging
-import pandas as pd
-import dash_bootstrap_components as dbc
-
-from flask import Flask
-from dash import dcc
-from dash import html
-from dash import callback_context, no_update, ALL
-from dash import Input, Output, State
-from dash.exceptions import PreventUpdate
-from yaml import load, FullLoader, YAMLError
-from copy import deepcopy
-from ast import literal_eval
-
-from ..utils.constants import Constants as C
-from ..utils.utils import show_tooltip, label, sync_checklists, list_tests, \
- gen_new_url
-from ..utils.url_processing import url_decode
-from ..data.data import Data
-from .graphs import graph_iterative, table_comparison, get_short_version, \
- select_iterative_data
-
-
-class Layout:
- """The layout of the dash app and the callbacks.
- """
-
- def __init__(self, app: Flask, releases: list, html_layout_file: str,
- graph_layout_file: str, data_spec_file: str, tooltip_file: str) -> None:
- """Initialization:
- - save the input parameters,
- - read and pre-process the data,
- - prepare data for the control panel,
- - read HTML layout file,
- - read tooltips from the tooltip file.
-
- :param app: Flask application running the dash application.
- :param releases: Lis of releases to be displayed.
- :param html_layout_file: Path and name of the file specifying the HTML
- layout of the dash application.
- :param graph_layout_file: Path and name of the file with layout of
- plot.ly graphs.
- :param data_spec_file: Path and name of the file specifying the data to
- be read from parquets for this application.
- :param tooltip_file: Path and name of the yaml file specifying the
- tooltips.
- :type app: Flask
- :type releases: list
- :type html_layout_file: str
- :type graph_layout_file: str
- :type data_spec_file: str
- :type tooltip_file: str
- """
-
- # Inputs
- self._app = app
- self.releases = releases
- self._html_layout_file = html_layout_file
- self._graph_layout_file = graph_layout_file
- self._data_spec_file = data_spec_file
- self._tooltip_file = tooltip_file
-
- # Read the data:
- self._data = pd.DataFrame()
- for rls in releases:
- data_mrr = Data(self._data_spec_file, True).\
- read_iterative_mrr(release=rls.replace("csit", "rls"))
- data_mrr["release"] = rls
- data_ndrpdr = Data(self._data_spec_file, True).\
- read_iterative_ndrpdr(release=rls.replace("csit", "rls"))
- data_ndrpdr["release"] = rls
- self._data = pd.concat(
- [self._data, data_mrr, data_ndrpdr], ignore_index=True)
-
- # Get structure of tests:
- tbs = dict()
- cols = ["job", "test_id", "test_type", "dut_version", "release"]
- for _, row in self._data[cols].drop_duplicates().iterrows():
- rls = row["release"]
- ttype = row["test_type"]
- lst_job = row["job"].split("-")
- dut = lst_job[1]
- d_ver = get_short_version(row["dut_version"], dut)
- tbed = "-".join(lst_job[-2:])
- lst_test_id = row["test_id"].split(".")
- if dut == "dpdk":
- area = "dpdk"
- else:
- area = "-".join(lst_test_id[3:-2])
- suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
- replace("2n-", "")
- test = lst_test_id[-1]
- nic = suite.split("-")[0]
- for drv in C.DRIVERS:
- if drv in test:
- driver = drv.replace("-", "_")
- test = test.replace(f"{drv}-", "")
- break
- else:
- driver = "dpdk"
- infra = "-".join((tbed, nic, driver))
- lst_test = test.split("-")
- framesize = lst_test[0]
- core = lst_test[1] if lst_test[1] else "8C"
- test = "-".join(lst_test[2: -1])
-
- if tbs.get(rls, None) is None:
- tbs[rls] = dict()
- if tbs[rls].get(dut, None) is None:
- tbs[rls][dut] = dict()
- if tbs[rls][dut].get(d_ver, None) is None:
- tbs[rls][dut][d_ver] = dict()
- if tbs[rls][dut][d_ver].get(infra, None) is None:
- tbs[rls][dut][d_ver][infra] = dict()
- if tbs[rls][dut][d_ver][infra].get(area, None) is None:
- tbs[rls][dut][d_ver][infra][area] = dict()
- if tbs[rls][dut][d_ver][infra][area].get(test, None) is None:
- tbs[rls][dut][d_ver][infra][area][test] = dict()
- tbs[rls][dut][d_ver][infra][area][test]["core"] = list()
- tbs[rls][dut][d_ver][infra][area][test]["frame-size"] = list()
- tbs[rls][dut][d_ver][infra][area][test]["test-type"] = list()
- if core.upper() not in \
- tbs[rls][dut][d_ver][infra][area][test]["core"]:
- tbs[rls][dut][d_ver][infra][area][test]["core"].append(
- core.upper())
- if framesize.upper() not in \
- tbs[rls][dut][d_ver][infra][area][test]["frame-size"]:
- tbs[rls][dut][d_ver][infra][area][test]["frame-size"].append(
- framesize.upper())
- if ttype == "mrr":
- if "MRR" not in \
- tbs[rls][dut][d_ver][infra][area][test]["test-type"]:
- tbs[rls][dut][d_ver][infra][area][test]["test-type"].append(
- "MRR")
- elif ttype == "ndrpdr":
- if "NDR" not in \
- tbs[rls][dut][d_ver][infra][area][test]["test-type"]:
- tbs[rls][dut][d_ver][infra][area][test]["test-type"].extend(
- ("NDR", "PDR", ))
- self._spec_tbs = tbs
-
- # Read from files:
- self._html_layout = ""
- self._graph_layout = None
- self._tooltips = dict()
-
- try:
- with open(self._html_layout_file, "r") as file_read:
- self._html_layout = file_read.read()
- except IOError as err:
- raise RuntimeError(
- f"Not possible to open the file {self._html_layout_file}\n{err}"
- )
-
- try:
- with open(self._graph_layout_file, "r") as file_read:
- self._graph_layout = load(file_read, Loader=FullLoader)
- except IOError as err:
- raise RuntimeError(
- f"Not possible to open the file {self._graph_layout_file}\n"
- f"{err}"
- )
- except YAMLError as err:
- raise RuntimeError(
- f"An error occurred while parsing the specification file "
- f"{self._graph_layout_file}\n{err}"
- )
-
- try:
- with open(self._tooltip_file, "r") as file_read:
- self._tooltips = load(file_read, Loader=FullLoader)
- except IOError as err:
- logging.warning(
- f"Not possible to open the file {self._tooltip_file}\n{err}"
- )
- except YAMLError as err:
- logging.warning(
- f"An error occurred while parsing the specification file "
- f"{self._tooltip_file}\n{err}"
- )
-
- # Callbacks:
- if self._app is not None and hasattr(self, 'callbacks'):
- self.callbacks(self._app)
-
- @property
- def html_layout(self):
- return self._html_layout
-
- @property
- def spec_tbs(self):
- return self._spec_tbs
-
- @property
- def data(self):
- return self._data
-
- @property
- def layout(self):
- return self._graph_layout
-
- def add_content(self):
- """Top level method which generated the web page.
-
- It generates:
- - Store for user input data,
- - Navigation bar,
- - Main area with control panel and ploting area.
-
- If no HTML layout is provided, an error message is displayed instead.
-
- :returns: The HTML div with the whole page.
- :rtype: html.Div
- """
-
- if self.html_layout and self.spec_tbs:
- return html.Div(
- id="div-main",
- children=[
- dbc.Row(
- id="row-navbar",
- class_name="g-0",
- children=[
- self._add_navbar(),
- ]
- ),
- dcc.Loading(
- dbc.Offcanvas(
- class_name="w-50",
- id="offcanvas-metadata",
- title="Throughput And Latency",
- placement="end",
- is_open=False,
- children=[
- dbc.Row(id="metadata-tput-lat"),
- dbc.Row(id="metadata-hdrh-graph"),
- ]
- )
- ),
- dbc.Row(
- id="row-main",
- class_name="g-0",
- children=[
- dcc.Store(id="selected-tests"),
- dcc.Store(id="control-panel"),
- dcc.Location(id="url", refresh=False),
- self._add_ctrl_col(),
- self._add_plotting_col(),
- ]
- )
- ]
- )
- else:
- return html.Div(
- id="div-main-error",
- children=[
- dbc.Alert(
- [
- "An Error Occured",
- ],
- color="danger",
- ),
- ]
- )
-
- def _add_navbar(self):
- """Add nav element with navigation panel. It is placed on the top.
-
- :returns: Navigation bar.
- :rtype: dbc.NavbarSimple
- """
- return dbc.NavbarSimple(
- id="navbarsimple-main",
- children=[
- dbc.NavItem(
- dbc.NavLink(
- "Iterative Test Runs",
- disabled=True,
- external_link=True,
- href="#"
- )
- )
- ],
- brand="Dashboard",
- brand_href="/",
- brand_external_link=True,
- class_name="p-2",
- fluid=True,
- )
-
- def _add_ctrl_col(self) -> dbc.Col:
- """Add column with controls. It is placed on the left side.
-
- :returns: Column with the control panel.
- :rtype: dbc.Col
- """
- return dbc.Col(
- id="col-controls",
- children=[
- self._add_ctrl_panel(),
- ],
- )
-
- def _add_plotting_col(self) -> dbc.Col:
- """Add column with plots and tables. It is placed on the right side.
-
- :returns: Column with tables.
- :rtype: dbc.Col
- """
- return dbc.Col(
- id="col-plotting-area",
- children=[
- dcc.Loading(
- children=[
- dbc.Row( # Graphs
- class_name="g-0 p-2",
- children=[
- dbc.Col(
- dbc.Row( # Throughput
- id="row-graph-tput",
- class_name="g-0 p-2",
- children=[C.PLACEHOLDER, ]
- ),
- width=6
- ),
- dbc.Col(
- dbc.Row( # Latency
- id="row-graph-lat",
- class_name="g-0 p-2",
- children=[C.PLACEHOLDER, ]
- ),
- width=6
- )
- ]
- ),
- dbc.Row( # Tables
- id="row-table",
- class_name="g-0 p-2",
- children=[C.PLACEHOLDER, ]
- ),
- dbc.Row( # Download
- id="row-btn-download",
- class_name="g-0 p-2",
- children=[C.PLACEHOLDER, ]
- )
- ]
- )
- ],
- width=9
- )
-
- def _add_ctrl_panel(self) -> dbc.Row:
- """Add control panel.
-
- :returns: Control panel.
- :rtype: dbc.Row
- """
- return dbc.Row(
- id="row-ctrl-panel",
- class_name="g-0 p-2",
- children=[
- dbc.Row(
- class_name="g-0",
- children=[
- dbc.InputGroup(
- [
- dbc.InputGroupText(
- children=show_tooltip(self._tooltips,
- "help-release", "CSIT Release")
- ),
- dbc.Select(
- id="dd-ctrl-rls",
- placeholder=("Select a Release..."),
- options=sorted(
- [
- {"label": k, "value": k} \
- for k in self.spec_tbs.keys()
- ],
- key=lambda d: d["label"]
- )
- )
- ],
- class_name="mb-3",
- size="sm",
- ),
- ]
- ),
- dbc.Row(
- class_name="g-0",
- children=[
- dbc.InputGroup(
- [
- dbc.InputGroupText(
- children=show_tooltip(self._tooltips,
- "help-dut", "DUT")
- ),
- dbc.Select(
- id="dd-ctrl-dut",
- placeholder=(
- "Select a Device under Test..."
- )
- )
- ],
- class_name="mb-3",
- size="sm",
- ),
- ]
- ),
- dbc.Row(
- class_name="g-0",
- children=[
- dbc.InputGroup(
- [
- dbc.InputGroupText(
- children=show_tooltip(self._tooltips,
- "help-dut-ver", "DUT Version")
- ),
- dbc.Select(
- id="dd-ctrl-dutver",
- placeholder=(
- "Select a Version of "
- "Device under Test..."
- )
- )
- ],
- class_name="mb-3",
- size="sm",
- ),
- ]
- ),
- dbc.Row(
- class_name="g-0",
- children=[
- dbc.InputGroup(
- [
- dbc.InputGroupText(
- children=show_tooltip(self._tooltips,
- "help-infra", "Infra")
- ),
- dbc.Select(
- id="dd-ctrl-phy",
- placeholder=(
- "Select a Physical Test Bed "
- "Topology..."
- )
- )
- ],
- class_name="mb-3",
- size="sm",
- ),
- ]
- ),
- dbc.Row(
- class_name="g-0",
- children=[
- dbc.InputGroup(
- [
- dbc.InputGroupText(
- children=show_tooltip(self._tooltips,
- "help-area", "Area")
- ),
- dbc.Select(
- id="dd-ctrl-area",
- placeholder="Select an Area...",
- disabled=True,
- ),
- ],
- class_name="mb-3",
- size="sm",
- ),
- ]
- ),
- dbc.Row(
- class_name="g-0",
- children=[
- dbc.InputGroup(
- [
- dbc.InputGroupText(
- children=show_tooltip(self._tooltips,
- "help-test", "Test")
- ),
- dbc.Select(
- id="dd-ctrl-test",
- placeholder="Select a Test...",
- disabled=True,
- ),
- ],
- class_name="mb-3",
- size="sm",
- ),
- ]
- ),
- dbc.Row(
- id="row-ctrl-framesize",
- class_name="gy-1",
- children=[
- dbc.Label(
- children=show_tooltip(self._tooltips,
- "help-framesize", "Frame Size"),
- class_name="p-0"
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-framesize-all",
- options=C.CL_ALL_DISABLED,
- inline=True,
- switch=False
- ),
- ],
- width=3
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-framesize",
- inline=True,
- switch=False
- )
- ]
- )
- ]
- ),
- dbc.Row(
- id="row-ctrl-core",
- class_name="gy-1",
- children=[
- dbc.Label(
- children=show_tooltip(self._tooltips,
- "help-cores", "Number of Cores"),
- class_name="p-0"
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-core-all",
- options=C.CL_ALL_DISABLED,
- inline=False,
- switch=False
- )
- ],
- width=3
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-core",
- inline=True,
- switch=False
- )
- ]
- )
- ]
- ),
- dbc.Row(
- id="row-ctrl-testtype",
- class_name="gy-1",
- children=[
- dbc.Label(
- children=show_tooltip(self._tooltips,
- "help-ttype", "Test Type"),
- class_name="p-0"
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-testtype-all",
- options=C.CL_ALL_DISABLED,
- inline=True,
- switch=False
- ),
- ],
- width=3
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-testtype",
- inline=True,
- switch=False
- )
- ]
- )
- ]
- ),
- dbc.Row(
- id="row-ctrl-normalize",
- class_name="gy-1",
- children=[
- dbc.Label(
- children=show_tooltip(self._tooltips,
- "help-normalize", "Normalize"),
- class_name="p-0"
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-normalize",
- options=[{
- "value": "normalize",
- "label": (
- "Normalize results to CPU"
- "frequency 2GHz"
- )
- }],
- value=[],
- inline=True,
- switch=False
- ),
- ]
- )
- ]
- ),
- dbc.Row(
- class_name="gy-1 p-0",
- children=[
- dbc.ButtonGroup(
- [
- dbc.Button(
- id="btn-ctrl-add",
- children="Add Selected",
- class_name="me-1",
- color="info"
- )
- ],
- size="md",
- )
- ]
- ),
- dbc.Row(
- id="row-card-sel-tests",
- class_name="gy-1",
- style=C.STYLE_DISABLED,
- children=[
- dbc.Label(
- "Selected tests",
- class_name="p-0"
- ),
- dbc.Checklist(
- class_name="overflow-auto",
- id="cl-selected",
- options=[],
- inline=False,
- style={"max-height": "20em"},
- )
- ],
- ),
- dbc.Row(
- id="row-btns-sel-tests",
- style=C.STYLE_DISABLED,
- children=[
- dbc.ButtonGroup(
- class_name="gy-2",
- children=[
- dbc.Button(
- id="btn-sel-remove",
- children="Remove Selected",
- class_name="w-100 me-1",
- color="info",
- disabled=False
- ),
- dbc.Button(
- id="btn-sel-remove-all",
- children="Remove All",
- class_name="w-100 me-1",
- color="info",
- disabled=False
- ),
- ],
- size="md",
- )
- ]
- ),
- ]
- )
-
- class ControlPanel:
- """A class representing the control panel.
- """
-
- def __init__(self, panel: dict) -> None:
- """Initialisation of the control pannel by default values. If
- particular values are provided (parameter "panel") they are set
- afterwards.
-
- :param panel: Custom values to be set to the control panel.
- :param default: Default values to be set to the control panel.
- :type panel: dict
- :type defaults: dict
- """
-
- # Defines also the order of keys
- self._defaults = {
- "dd-rls-value": str(),
- "dd-dut-options": list(),
- "dd-dut-disabled": True,
- "dd-dut-value": str(),
- "dd-dutver-options": list(),
- "dd-dutver-disabled": True,
- "dd-dutver-value": str(),
- "dd-phy-options": list(),
- "dd-phy-disabled": True,
- "dd-phy-value": str(),
- "dd-area-options": list(),
- "dd-area-disabled": True,
- "dd-area-value": str(),
- "dd-test-options": list(),
- "dd-test-disabled": True,
- "dd-test-value": str(),
- "cl-core-options": list(),
- "cl-core-value": list(),
- "cl-core-all-value": list(),
- "cl-core-all-options": C.CL_ALL_DISABLED,
- "cl-framesize-options": list(),
- "cl-framesize-value": list(),
- "cl-framesize-all-value": list(),
- "cl-framesize-all-options": C.CL_ALL_DISABLED,
- "cl-testtype-options": list(),
- "cl-testtype-value": list(),
- "cl-testtype-all-value": list(),
- "cl-testtype-all-options": C.CL_ALL_DISABLED,
- "btn-add-disabled": True,
- "cl-normalize-value": list(),
- "cl-selected-options": list()
- }
-
- self._panel = deepcopy(self._defaults)
- if panel:
- for key in self._defaults:
- self._panel[key] = panel[key]
-
- @property
- def defaults(self) -> dict:
- return self._defaults
-
- @property
- def panel(self) -> dict:
- return self._panel
-
- def set(self, kwargs: dict) -> None:
- """Set the values of the Control panel.
-
- :param kwargs: key - value pairs to be set.
- :type kwargs: dict
- :raises KeyError: If the key in kwargs is not present in the Control
- panel.
- """
- for key, val in kwargs.items():
- if key in self._panel:
- self._panel[key] = val
- else:
- raise KeyError(f"The key {key} is not defined.")
-
- def get(self, key: str) -> any:
- """Returns the value of a key from the Control panel.
-
- :param key: The key which value should be returned.
- :type key: str
- :returns: The value of the key.
- :rtype: any
- :raises KeyError: If the key in kwargs is not present in the Control
- panel.
- """
- return self._panel[key]
-
- def values(self) -> tuple:
- """Returns the values from the Control panel as a list.
-
- :returns: The values from the Control panel.
- :rtype: list
- """
- return tuple(self._panel.values())
-
- def callbacks(self, app):
- """Callbacks for the whole application.
-
- :param app: The application.
- :type app: Flask
- """
-
- def _generate_plotting_area(figs: tuple, table: pd.DataFrame,
- url: str) -> tuple:
- """Generate the plotting area with all its content.
-
- :param figs: Figures to be placed in the plotting area.
- :param table: A table to be placed in the plotting area bellow the
- figures.
- :param utl: The URL to be placed in the plotting area bellow the
- tables.
- :type figs: tuple of plotly.graph_objects.Figure
- :type table: pandas.DataFrame
- :type url: str
- :returns: tuple of elements to be shown in the plotting area.
- :rtype: tuple
- (dcc.Graph, dcc.Graph, dbc.Table, list(dbc.Col, dbc.Col))
- """
-
- (fig_tput, fig_lat) = figs
-
- row_fig_tput = C.PLACEHOLDER
- row_fig_lat = C.PLACEHOLDER
- row_table = C.PLACEHOLDER
- row_btn_dwnld = C.PLACEHOLDER
-
- if fig_tput:
- row_fig_tput = [
- dcc.Graph(
- id={"type": "graph", "index": "tput"},
- figure=fig_tput
- )
- ]
- row_btn_dwnld = [
- dbc.Col( # Download
- width=2,
- children=[
- dcc.Loading(children=[
- dbc.Button(
- id="btn-download-data",
- children=show_tooltip(self._tooltips,
- "help-download", "Download Data"),
- class_name="me-1",
- color="info"
- ),
- dcc.Download(id="download-data")
- ]),
- ]
- ),
- dbc.Col( # Show URL
- width=10,
- children=[
- dbc.InputGroup(
- class_name="me-1",
- children=[
- dbc.InputGroupText(
- style=C.URL_STYLE,
- children=show_tooltip(self._tooltips,
- "help-url", "URL", "input-url")
- ),
- dbc.Input(
- id="input-url",
- readonly=True,
- type="url",
- style=C.URL_STYLE,
- value=url
- )
- ]
- )
- ]
- )
- ]
- if fig_lat:
- row_fig_lat = [
- dcc.Graph(
- id={"type": "graph", "index": "lat"},
- figure=fig_lat
- )
- ]
- if not table.empty:
- row_table = [
- dbc.Table.from_dataframe(
- table,
- id={"type": "table", "index": "compare"},
- striped=True,
- bordered=True,
- hover=True
- )
- ]
-
- return row_fig_tput, row_fig_lat, row_table, row_btn_dwnld
-
- @app.callback(
- Output("control-panel", "data"), # Store
- Output("selected-tests", "data"), # Store
- Output("row-graph-tput", "children"),
- Output("row-graph-lat", "children"),
- Output("row-table", "children"),
- Output("row-btn-download", "children"),
- Output("row-card-sel-tests", "style"),
- Output("row-btns-sel-tests", "style"),
- Output("dd-ctrl-rls", "value"),
- Output("dd-ctrl-dut", "options"),
- Output("dd-ctrl-dut", "disabled"),
- Output("dd-ctrl-dut", "value"),
- Output("dd-ctrl-dutver", "options"),
- Output("dd-ctrl-dutver", "disabled"),
- Output("dd-ctrl-dutver", "value"),
- Output("dd-ctrl-phy", "options"),
- Output("dd-ctrl-phy", "disabled"),
- Output("dd-ctrl-phy", "value"),
- Output("dd-ctrl-area", "options"),
- Output("dd-ctrl-area", "disabled"),
- Output("dd-ctrl-area", "value"),
- Output("dd-ctrl-test", "options"),
- Output("dd-ctrl-test", "disabled"),
- Output("dd-ctrl-test", "value"),
- Output("cl-ctrl-core", "options"),
- Output("cl-ctrl-core", "value"),
- Output("cl-ctrl-core-all", "value"),
- Output("cl-ctrl-core-all", "options"),
- Output("cl-ctrl-framesize", "options"),
- Output("cl-ctrl-framesize", "value"),
- Output("cl-ctrl-framesize-all", "value"),
- Output("cl-ctrl-framesize-all", "options"),
- Output("cl-ctrl-testtype", "options"),
- Output("cl-ctrl-testtype", "value"),
- Output("cl-ctrl-testtype-all", "value"),
- Output("cl-ctrl-testtype-all", "options"),
- Output("btn-ctrl-add", "disabled"),
- Output("cl-ctrl-normalize", "value"),
- Output("cl-selected", "options"), # User selection
- State("control-panel", "data"), # Store
- State("selected-tests", "data"), # Store
- State("cl-selected", "value"), # User selection
- Input("dd-ctrl-rls", "value"),
- Input("dd-ctrl-dut", "value"),
- Input("dd-ctrl-dutver", "value"),
- Input("dd-ctrl-phy", "value"),
- Input("dd-ctrl-area", "value"),
- Input("dd-ctrl-test", "value"),
- Input("cl-ctrl-core", "value"),
- Input("cl-ctrl-core-all", "value"),
- Input("cl-ctrl-framesize", "value"),
- Input("cl-ctrl-framesize-all", "value"),
- Input("cl-ctrl-testtype", "value"),
- Input("cl-ctrl-testtype-all", "value"),
- Input("cl-ctrl-normalize", "value"),
- Input("btn-ctrl-add", "n_clicks"),
- Input("btn-sel-remove", "n_clicks"),
- Input("btn-sel-remove-all", "n_clicks"),
- Input("url", "href")
- )
- def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
- dd_rls: str, dd_dut: str, dd_dutver: str, dd_phy: str, dd_area: str,
- dd_test: str, cl_core: list, cl_core_all: list, cl_framesize: list,
- cl_framesize_all: list, cl_testtype: list, cl_testtype_all: list,
- cl_normalize: list, btn_add: int, btn_remove: int,
- btn_remove_all: int, href: str) -> tuple:
- """Update the application when the event is detected.
-
- :param cp_data: Current status of the control panel stored in
- browser.
- :param store_sel: List of tests selected by user stored in the
- browser.
- :param list_sel: List of tests selected by the user shown in the
- checklist.
- :param dd_rls: Input - Releases.
- :param dd_dut: Input - DUTs.
- :param dd_dutver: Input - Version of DUT.
- :param dd_phy: Input - topo- arch-nic-driver.
- :param dd_area: Input - Tested area.
- :param dd_test: Input - Test.
- :param cl_core: Input - Number of cores.
- :param cl_core_all: Input - All numbers of cores.
- :param cl_framesize: Input - Frame sizes.
- :param cl_framesize_all: Input - All frame sizes.
- :param cl_testtype: Input - Test type (NDR, PDR, MRR).
- :param cl_testtype_all: Input - All test types.
- :param cl_normalize: Input - Normalize the results.
- :param btn_add: Input - Button "Add Selected" tests.
- :param btn_remove: Input - Button "Remove selected" tests.
- :param btn_remove_all: Input - Button "Remove All" tests.
- :param href: Input - The URL provided by the browser.
- :type cp_data: dict
- :type store_sel: list
- :type list_sel: list
- :type dd_rls: str
- :type dd_dut: str
- :type dd_dutver: str
- :type dd_phy: str
- :type dd_area: str
- :type dd_test: str
- :type cl_core: list
- :type cl_core_all: list
- :type cl_framesize: list
- :type cl_framesize_all: list
- :type cl_testtype: list
- :type cl_testtype_all: list
- :type cl_normalize: list
- :type btn_add: int
- :type btn_remove: int
- :type btn_remove_all: int
- :type href: str
- :returns: New values for web page elements.
- :rtype: tuple
- """
-
- ctrl_panel = self.ControlPanel(cp_data)
-
- # Parse the url:
- parsed_url = url_decode(href)
-
- row_fig_tput = no_update
- row_fig_lat = no_update
- row_table = no_update
- row_btn_dwnld = no_update
- row_card_sel_tests = no_update
- row_btns_sel_tests = no_update
-
- trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
-
- if trigger_id == "dd-ctrl-rls":
- try:
- rls = self.spec_tbs[dd_rls]
- options = sorted(
- [{"label": v, "value": v} for v in rls.keys()],
- key=lambda d: d["label"]
- )
- disabled = False
- except KeyError:
- options = list()
- disabled = True
- ctrl_panel.set({
- "dd-rls-value": dd_rls,
- "dd-dut-value": str(),
- "dd-dut-options": options,
- "dd-dut-disabled": disabled,
- "dd-dutver-value": str(),
- "dd-dutver-options": list(),
- "dd-dutver-disabled": True,
- "dd-phy-value": str(),
- "dd-phy-options": list(),
- "dd-phy-disabled": True,
- "dd-area-value": str(),
- "dd-area-options": list(),
- "dd-area-disabled": True,
- "dd-test-value": str(),
- "dd-test-options": list(),
- "dd-test-disabled": True,
- "cl-core-options": list(),
- "cl-core-value": list(),
- "cl-core-all-value": list(),
- "cl-core-all-options": C.CL_ALL_DISABLED,
- "cl-framesize-options": list(),
- "cl-framesize-value": list(),
- "cl-framesize-all-value": list(),
- "cl-framesize-all-options": C.CL_ALL_DISABLED,
- "cl-testtype-options": list(),
- "cl-testtype-value": list(),
- "cl-testtype-all-value": list(),
- "cl-testtype-all-options": C.CL_ALL_DISABLED
- })
- elif trigger_id == "dd-ctrl-dut":
- try:
- rls = ctrl_panel.get("dd-rls-value")
- dut = self.spec_tbs[rls][dd_dut]
- options = sorted(
- [{"label": v, "value": v} for v in dut.keys()],
- key=lambda d: d["label"]
- )
- disabled = False
- except KeyError:
- options = list()
- disabled = True
- ctrl_panel.set({
- "dd-dut-value": dd_dut,
- "dd-dutver-value": str(),
- "dd-dutver-options": options,
- "dd-dutver-disabled": disabled,
- "dd-phy-value": str(),
- "dd-phy-options": list(),
- "dd-phy-disabled": True,
- "dd-area-value": str(),
- "dd-area-options": list(),
- "dd-area-disabled": True,
- "dd-test-value": str(),
- "dd-test-options": list(),
- "dd-test-disabled": True,
- "cl-core-options": list(),
- "cl-core-value": list(),
- "cl-core-all-value": list(),
- "cl-core-all-options": C.CL_ALL_DISABLED,
- "cl-framesize-options": list(),
- "cl-framesize-value": list(),
- "cl-framesize-all-value": list(),
- "cl-framesize-all-options": C.CL_ALL_DISABLED,
- "cl-testtype-options": list(),
- "cl-testtype-value": list(),
- "cl-testtype-all-value": list(),
- "cl-testtype-all-options": C.CL_ALL_DISABLED
- })
- elif trigger_id == "dd-ctrl-dutver":
- try:
- rls = ctrl_panel.get("dd-rls-value")
- dut = ctrl_panel.get("dd-dut-value")
- dutver = self.spec_tbs[rls][dut][dd_dutver]
- options = sorted(
- [{"label": v, "value": v} for v in dutver.keys()],
- key=lambda d: d["label"]
- )
- disabled = False
- except KeyError:
- options = list()
- disabled = True
- ctrl_panel.set({
- "dd-dutver-value": dd_dutver,
- "dd-phy-value": str(),
- "dd-phy-options": options,
- "dd-phy-disabled": disabled,
- "dd-area-value": str(),
- "dd-area-options": list(),
- "dd-area-disabled": True,
- "dd-test-value": str(),
- "dd-test-options": list(),
- "dd-test-disabled": True,
- "cl-core-options": list(),
- "cl-core-value": list(),
- "cl-core-all-value": list(),
- "cl-core-all-options": C.CL_ALL_DISABLED,
- "cl-framesize-options": list(),
- "cl-framesize-value": list(),
- "cl-framesize-all-value": list(),
- "cl-framesize-all-options": C.CL_ALL_DISABLED,
- "cl-testtype-options": list(),
- "cl-testtype-value": list(),
- "cl-testtype-all-value": list(),
- "cl-testtype-all-options": C.CL_ALL_DISABLED
- })
- elif trigger_id == "dd-ctrl-phy":
- try:
- rls = ctrl_panel.get("dd-rls-value")
- dut = ctrl_panel.get("dd-dut-value")
- dutver = ctrl_panel.get("dd-dutver-value")
- phy = self.spec_tbs[rls][dut][dutver][dd_phy]
- options = sorted(
- [{"label": label(v), "value": v} for v in phy.keys()],
- key=lambda d: d["label"]
- )
- disabled = False
- except KeyError:
- options = list()
- disabled = True
- ctrl_panel.set({
- "dd-phy-value": dd_phy,
- "dd-area-value": str(),
- "dd-area-options": options,
- "dd-area-disabled": disabled,
- "dd-test-value": str(),
- "dd-test-options": list(),
- "dd-test-disabled": True,
- "cl-core-options": list(),
- "cl-core-value": list(),
- "cl-core-all-value": list(),
- "cl-core-all-options": C.CL_ALL_DISABLED,
- "cl-framesize-options": list(),
- "cl-framesize-value": list(),
- "cl-framesize-all-value": list(),
- "cl-framesize-all-options": C.CL_ALL_DISABLED,
- "cl-testtype-options": list(),
- "cl-testtype-value": list(),
- "cl-testtype-all-value": list(),
- "cl-testtype-all-options": C.CL_ALL_DISABLED
- })
- elif trigger_id == "dd-ctrl-area":
- try:
- rls = ctrl_panel.get("dd-rls-value")
- dut = ctrl_panel.get("dd-dut-value")
- dutver = ctrl_panel.get("dd-dutver-value")
- phy = ctrl_panel.get("dd-phy-value")
- area = self.spec_tbs[rls][dut][dutver][phy][dd_area]
- options = sorted(
- [{"label": v, "value": v} for v in area.keys()],
- key=lambda d: d["label"]
- )
- disabled = False
- except KeyError:
- options = list()
- disabled = True
- ctrl_panel.set({
- "dd-area-value": dd_area,
- "dd-test-value": str(),
- "dd-test-options": options,
- "dd-test-disabled": disabled,
- "cl-core-options": list(),
- "cl-core-value": list(),
- "cl-core-all-value": list(),
- "cl-core-all-options": C.CL_ALL_DISABLED,
- "cl-framesize-options": list(),
- "cl-framesize-value": list(),
- "cl-framesize-all-value": list(),
- "cl-framesize-all-options": C.CL_ALL_DISABLED,
- "cl-testtype-options": list(),
- "cl-testtype-value": list(),
- "cl-testtype-all-value": list(),
- "cl-testtype-all-options": C.CL_ALL_DISABLED
- })
- elif trigger_id == "dd-ctrl-test":
- rls = ctrl_panel.get("dd-rls-value")
- dut = ctrl_panel.get("dd-dut-value")
- dutver = ctrl_panel.get("dd-dutver-value")
- phy = ctrl_panel.get("dd-phy-value")
- area = ctrl_panel.get("dd-area-value")
- test = self.spec_tbs[rls][dut][dutver][phy][area][dd_test]
- if dut and phy and area and dd_test:
- ctrl_panel.set({
- "dd-test-value": dd_test,
- "cl-core-options": [{"label": v, "value": v}
- for v in sorted(test["core"])],
- "cl-core-value": list(),
- "cl-core-all-value": list(),
- "cl-core-all-options": C.CL_ALL_ENABLED,
- "cl-framesize-options": [{"label": v, "value": v}
- for v in sorted(test["frame-size"])],
- "cl-framesize-value": list(),
- "cl-framesize-all-value": list(),
- "cl-framesize-all-options": C.CL_ALL_ENABLED,
- "cl-testtype-options": [{"label": v, "value": v}
- for v in sorted(test["test-type"])],
- "cl-testtype-value": list(),
- "cl-testtype-all-value": list(),
- "cl-testtype-all-options": C.CL_ALL_ENABLED,
- })
- elif trigger_id == "cl-ctrl-core":
- val_sel, val_all = sync_checklists(
- options=ctrl_panel.get("cl-core-options"),
- sel=cl_core,
- all=list(),
- id=""
- )
- ctrl_panel.set({
- "cl-core-value": val_sel,
- "cl-core-all-value": val_all,
- })
- elif trigger_id == "cl-ctrl-core-all":
- val_sel, val_all = sync_checklists(
- options = ctrl_panel.get("cl-core-options"),
- sel=list(),
- all=cl_core_all,
- id="all"
- )
- ctrl_panel.set({
- "cl-core-value": val_sel,
- "cl-core-all-value": val_all,
- })
- elif trigger_id == "cl-ctrl-framesize":
- val_sel, val_all = sync_checklists(
- options = ctrl_panel.get("cl-framesize-options"),
- sel=cl_framesize,
- all=list(),
- id=""
- )
- ctrl_panel.set({
- "cl-framesize-value": val_sel,
- "cl-framesize-all-value": val_all,
- })
- elif trigger_id == "cl-ctrl-framesize-all":
- val_sel, val_all = sync_checklists(
- options = ctrl_panel.get("cl-framesize-options"),
- sel=list(),
- all=cl_framesize_all,
- id="all"
- )
- ctrl_panel.set({
- "cl-framesize-value": val_sel,
- "cl-framesize-all-value": val_all,
- })
- elif trigger_id == "cl-ctrl-testtype":
- val_sel, val_all = sync_checklists(
- options = ctrl_panel.get("cl-testtype-options"),
- sel=cl_testtype,
- all=list(),
- id=""
- )
- ctrl_panel.set({
- "cl-testtype-value": val_sel,
- "cl-testtype-all-value": val_all,
- })
- elif trigger_id == "cl-ctrl-testtype-all":
- val_sel, val_all = sync_checklists(
- options = ctrl_panel.get("cl-testtype-options"),
- sel=list(),
- all=cl_testtype_all,
- id="all"
- )
- ctrl_panel.set({
- "cl-testtype-value": val_sel,
- "cl-testtype-all-value": val_all,
- })
- elif trigger_id == "btn-ctrl-add":
- _ = btn_add
- rls = ctrl_panel.get("dd-rls-value")
- dut = ctrl_panel.get("dd-dut-value")
- dutver = ctrl_panel.get("dd-dutver-value")
- phy = ctrl_panel.get("dd-phy-value")
- area = ctrl_panel.get("dd-area-value")
- test = ctrl_panel.get("dd-test-value")
- cores = ctrl_panel.get("cl-core-value")
- framesizes = ctrl_panel.get("cl-framesize-value")
- testtypes = ctrl_panel.get("cl-testtype-value")
- # Add selected test to the list of tests in store:
- if all((rls, dut, dutver, phy, area, test, cores, framesizes,
- testtypes)):
- if store_sel is None:
- store_sel = list()
- for core in cores:
- for framesize in framesizes:
- for ttype in testtypes:
- if dut == "trex":
- core = str()
- tid = "-".join((rls, dut, dutver,
- phy.replace('af_xdp', 'af-xdp'), area,
- framesize.lower(), core.lower(), test,
- ttype.lower()))
- if tid not in [itm["id"] for itm in store_sel]:
- store_sel.append({
- "id": tid,
- "rls": rls,
- "dut": dut,
- "dutver": dutver,
- "phy": phy,
- "area": area,
- "test": test,
- "framesize": framesize.lower(),
- "core": core.lower(),
- "testtype": ttype.lower()
- })
- store_sel = sorted(store_sel, key=lambda d: d["id"])
- row_card_sel_tests = C.STYLE_ENABLED
- row_btns_sel_tests = C.STYLE_ENABLED
- if C.CLEAR_ALL_INPUTS:
- ctrl_panel.set(ctrl_panel.defaults)
- ctrl_panel.set({
- "cl-selected-options": list_tests(store_sel)
- })
- elif trigger_id == "btn-sel-remove-all":
- _ = btn_remove_all
- row_fig_tput = C.PLACEHOLDER
- row_fig_lat = C.PLACEHOLDER
- row_table = C.PLACEHOLDER
- row_btn_dwnld = C.PLACEHOLDER
- row_card_sel_tests = C.STYLE_DISABLED
- row_btns_sel_tests = C.STYLE_DISABLED
- store_sel = list()
- ctrl_panel.set({"cl-selected-options": list()})
- elif trigger_id == "btn-sel-remove":
- _ = btn_remove
- if list_sel:
- new_store_sel = list()
- for item in store_sel:
- if item["id"] not in list_sel:
- new_store_sel.append(item)
- store_sel = new_store_sel
- elif trigger_id == "url":
- # TODO: Add verification
- url_params = parsed_url["params"]
- if url_params:
- store_sel = literal_eval(
- url_params.get("store_sel", list())[0])
- if store_sel:
- row_card_sel_tests = C.STYLE_ENABLED
- row_btns_sel_tests = C.STYLE_ENABLED
-
- if trigger_id in ("btn-ctrl-add", "url", "btn-sel-remove",
- "cl-ctrl-normalize"):
- if store_sel:
- row_fig_tput, row_fig_lat, row_table, row_btn_dwnld = \
- _generate_plotting_area(
- graph_iterative(
- self.data, store_sel, self.layout,
- bool(cl_normalize)
- ),
- table_comparison(
- self.data, store_sel, bool(cl_normalize)
- ),
- gen_new_url(parsed_url, {"store_sel": store_sel})
- )
- ctrl_panel.set({
- "cl-selected-options": list_tests(store_sel)
- })
- else:
- row_fig_tput = C.PLACEHOLDER
- row_fig_lat = C.PLACEHOLDER
- row_table = C.PLACEHOLDER
- row_btn_dwnld = C.PLACEHOLDER
- row_card_sel_tests = C.STYLE_DISABLED
- row_btns_sel_tests = C.STYLE_DISABLED
- store_sel = list()
- ctrl_panel.set({"cl-selected-options": list()})
-
- if ctrl_panel.get("cl-core-value") and \
- ctrl_panel.get("cl-framesize-value") and \
- ctrl_panel.get("cl-testtype-value"):
- disabled = False
- else:
- disabled = True
- ctrl_panel.set({
- "btn-add-disabled": disabled,
- "cl-normalize-value": cl_normalize
- })
-
- ret_val = [
- ctrl_panel.panel, store_sel,
- row_fig_tput, row_fig_lat, row_table, row_btn_dwnld,
- row_card_sel_tests, row_btns_sel_tests
- ]
- ret_val.extend(ctrl_panel.values())
- return ret_val
-
- @app.callback(
- Output("download-data", "data"),
- State("selected-tests", "data"),
- Input("btn-download-data", "n_clicks"),
- prevent_initial_call=True
- )
- def _download_data(store_sel, n_clicks):
- """Download the data
-
- :param store_sel: List of tests selected by user stored in the
- browser.
- :param n_clicks: Number of clicks on the button "Download".
- :type store_sel: list
- :type n_clicks: int
- :returns: dict of data frame content (base64 encoded) and meta data
- used by the Download component.
- :rtype: dict
- """
-
- if not n_clicks:
- raise PreventUpdate
-
- if not store_sel:
- raise PreventUpdate
-
- df = pd.DataFrame()
- for itm in store_sel:
- sel_data = select_iterative_data(self.data, itm)
- if sel_data is None:
- continue
- df = pd.concat([df, sel_data], ignore_index=True)
-
- return dcc.send_data_frame(df.to_csv, C.REPORT_DOWNLOAD_FILE_NAME)
diff --git a/resources/tools/dash/app/pal/report/layout.yaml b/resources/tools/dash/app/pal/report/layout.yaml
deleted file mode 100644
index 689a91d291..0000000000
--- a/resources/tools/dash/app/pal/report/layout.yaml
+++ /dev/null
@@ -1,240 +0,0 @@
-plot-throughput:
- xaxis:
- title: "Test Cases [Index]"
- autorange: True
- fixedrange: False
- gridcolor: "rgb(230, 230, 230)"
- linecolor: "rgb(220, 220, 220)"
- linewidth: 1
- showgrid: True
- showline: True
- showticklabels: True
- tickcolor: "rgb(220, 220, 220)"
- tickmode: "array"
- zeroline: False
- yaxis:
- title: "Packet Throughput [pps]"
- gridcolor: "rgb(230, 230, 230)"
- hoverformat: ".3s"
- tickformat: ".3s"
- linecolor: "rgb(220, 220, 220)"
- linewidth: 1
- showgrid: True
- showline: True
- showticklabels: True
- tickcolor: "rgb(220, 220, 220)"
- zeroline: False
- range: [0, 50]
- autosize: False
- margin:
- t: 50
- b: 0
- l: 80
- r: 20
- showlegend: True
- legend:
- orientation: "h"
- font:
- size: 10
- width: 700
- height: 900
- paper_bgcolor: "#fff"
- plot_bgcolor: "#fff"
- hoverlabel:
- namelength: -1
-
-plot-latency:
- xaxis:
- title: "Test Cases [Index]"
- autorange: True
- fixedrange: False
- gridcolor: "rgb(230, 230, 230)"
- linecolor: "rgb(220, 220, 220)"
- linewidth: 1
- showgrid: True
- showline: True
- showticklabels: True
- tickcolor: "rgb(220, 220, 220)"
- tickmode: "array"
- zeroline: False
- yaxis:
- title: "Average Latency at 50% PDR [us]"
- gridcolor: "rgb(230, 230, 230)"
- hoverformat: ".3s"
- tickformat: ".3s"
- linecolor: "rgb(220, 220, 220)"
- linewidth: 1
- showgrid: True
- showline: True
- showticklabels: True
- tickcolor: "rgb(220, 220, 220)"
- zeroline: False
- range: [0, 50]
- autosize: False
- margin:
- t: 50
- b: 0
- l: 80
- r: 20
- showlegend: True
- legend:
- orientation: "h"
- font:
- size: 10
- width: 700
- height: 900
- paper_bgcolor: "#fff"
- plot_bgcolor: "#fff"
- hoverlabel:
- namelength: -1
-
-plot-hdrh-latency:
- # title:
- # text: "Latency by Percentile Distribution"
- # xanchor: "center"
- # x: 0.5
- # font:
- # size: 10
- showlegend: True
- legend:
- traceorder: "normal"
- orientation: "h"
- # font:
- # size: 16
- xanchor: "left"
- yanchor: "top"
- x: 0
- y: -0.25
- bgcolor: "rgba(255, 255, 255, 0)"
- bordercolor: "rgba(255, 255, 255, 0)"
- xaxis:
- type: "log"
- title: "Percentile [%]"
- # titlefont:
- # size: 14
- autorange: False
- fixedrange: True
- gridcolor: "rgb(230, 230, 230)"
- linecolor: "rgb(220, 220, 220)"
- linewidth: 1
- showgrid: True
- showline: True
- showticklabels: True
- tickcolor: "rgb(220, 220, 220)"
- tickvals: [1, 2, 1e1, 20, 1e2, 1e3, 1e4, 1e5, 1e6]
- ticktext: [0, 50, 90, 95, 99, 99.9, 99.99, 99.999, 99.9999]
- # tickfont:
- # size: 14
- yaxis:
- title: "One-Way Latency per Direction [us]"
- # titlefont:
- # size: 14
- gridcolor: "rgb(230, 230, 230)"
- linecolor: "rgb(220, 220, 220)"
- linewidth: 1
- showgrid: True
- showline: True
- showticklabels: True
- tickcolor: "rgb(220, 220, 220)"
- # tickfont:
- # size: 14
- autosize: True
- #height: 400
- paper_bgcolor: "white"
- plot_bgcolor: "white"
-
-plot-throughput-speedup-analysis:
- titlefont:
- size: 16
- xaxis:
- title: "<b>Number of Cores [Qty]</b>"
- titlefont:
- size: 14
- autorange: True
- fixedrange: False
- gridcolor: "rgb(230, 230, 230)"
- linecolor: "rgb(220, 220, 220)"
- linewidth: 1
- showgrid: True
- showline: True
- showticklabels: True
- tickcolor: "rgb(238, 238, 238)"
- tickmode: "linear"
- tickfont:
- size: 14
- zeroline: False
- yaxis:
- title: "<b>Packet Throughput [Mpps]</b>"
- titlefont:
- size: 14
- type: "linear"
- gridcolor: "rgb(230, 230, 230)"
- hoverformat: ".4s"
- linecolor: "rgb(220, 220, 220)"
- linewidth: 1
- showgrid: True
- showline: True
- showticklabels: True
- tickcolor: "rgb(220, 220, 220)"
- tickformat: ".4s"
- tickfont:
- size: 14
- zeroline: True
- rangemode: "tozero"
- range: [0, 100]
- legend:
- orientation: "h"
- font:
- size: 14
- xanchor: "left"
- yanchor: "top"
- x: 0
- y: -0.2
- bgcolor: "rgba(255, 255, 255, 0)"
- bordercolor: "rgba(255, 255, 255, 0)"
- traceorder: "normal"
- autosize: False
- margin:
- 't': 50
- 'b': 150
- 'l': 85
- 'r': 10
- showlegend: True
- width: 700
- height: 700
- paper_bgcolor: "#fff"
- plot_bgcolor: "#fff"
- hoverlabel:
- namelength: -1
- annotations: [
- {
- text: "_ _ __ ...",
- align: "left",
- showarrow: False,
- xref: "paper",
- yref: "paper",
- xanchor: "left",
- yanchor: "top",
- x: 0,
- y: -0.14,
- font: {
- family: "Consolas, Courier New",
- size: 13
- },
- },
- {
- text: " Perfect Measured Limit",
- align: "left",
- showarrow: False,
- xref: "paper",
- yref: "paper",
- xanchor: "left",
- yanchor: "top",
- x: 0,
- y: -0.15,
- font: {
- family: "Consolas, Courier New",
- size: 13
- },
- },
- ]
diff --git a/resources/tools/dash/app/pal/report/report.py b/resources/tools/dash/app/pal/report/report.py
deleted file mode 100644
index e4565731ec..0000000000
--- a/resources/tools/dash/app/pal/report/report.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Instantiate the Report Dash application.
-"""
-import dash
-
-from ..utils.constants import Constants as C
-from .layout import Layout
-
-
-def init_report(server, releases):
- """Create a Plotly Dash dashboard.
-
- :param server: Flask server.
- :type server: Flask
- :returns: Dash app server.
- :rtype: Dash
- """
-
- dash_app = dash.Dash(
- server=server,
- routes_pathname_prefix=C.REPORT_ROUTES_PATHNAME_PREFIX,
- external_stylesheets=C.EXTERNAL_STYLESHEETS
- )
-
- layout = Layout(
- app=dash_app,
- releases=releases,
- html_layout_file=C.REPORT_HTML_LAYOUT_FILE,
- graph_layout_file=C.REPORT_GRAPH_LAYOUT_FILE,
- data_spec_file=C.DATA_SPEC_FILE,
- tooltip_file=C.TOOLTIP_FILE,
- )
- dash_app.index_string = layout.html_layout
- dash_app.layout = layout.add_content()
-
- return dash_app.server
diff --git a/resources/tools/dash/app/pal/routes.py b/resources/tools/dash/app/pal/routes.py
deleted file mode 100644
index 59af748168..0000000000
--- a/resources/tools/dash/app/pal/routes.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Routes for parent Flask app.
-"""
-
-from flask import current_app as app
-from flask import render_template
-
-from .utils.constants import Constants as C
-
-
-@app.route(C.APPLICATIN_ROOT)
-def home():
- """Landing page.
- """
- return render_template(
- C.MAIN_HTML_LAYOUT_FILE,
- title=C.TITLE,
- description=C.DESCRIPTION,
- template=C.TEMPLATE
- )
diff --git a/resources/tools/dash/app/pal/static/dist/css/bootstrap.min.css b/resources/tools/dash/app/pal/static/dist/css/bootstrap.min.css
deleted file mode 100644
index a7c5612a62..0000000000
--- a/resources/tools/dash/app/pal/static/dist/css/bootstrap.min.css
+++ /dev/null
@@ -1,12 +0,0 @@
-/*!
- * Bootswatch v5.1.3
- * Homepage: https://bootswatch.com
- * Copyright 2012-2021 Thomas Park
- * Licensed under MIT
- * Based on Bootstrap
-*//*!
- * Bootstrap v5.1.3 (https://getbootstrap.com/)
- * Copyright 2011-2021 The Bootstrap Authors
- * Copyright 2011-2021 Twitter, Inc.
- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
- */@import url(https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600&display=swap);:root{--bs-blue:#007bff;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#e83e8c;--bs-red:#d9534f;--bs-orange:#fd7e14;--bs-yellow:#f0ad4e;--bs-green:#4bbf73;--bs-teal:#20c997;--bs-cyan:#1f9bcf;--bs-white:#fff;--bs-gray:#919aa1;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#f7f7f9;--bs-gray-300:#eceeef;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#919aa1;--bs-gray-700:#55595c;--bs-gray-800:#343a40;--bs-gray-900:#1a1a1a;--bs-primary:#1a1a1a;--bs-secondary:#fff;--bs-success:#4bbf73;--bs-info:#1f9bcf;--bs-warning:#f0ad4e;--bs-danger:#d9534f;--bs-light:#fff;--bs-dark:#343a40;--bs-primary-rgb:26,26,26;--bs-secondary-rgb:255,255,255;--bs-success-rgb:75,191,115;--bs-info-rgb:31,155,207;--bs-warning-rgb:240,173,78;--bs-danger-rgb:217,83,79;--bs-light-rgb:255,255,255;--bs-dark-rgb:52,58,64;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:85,89,92;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:"Nunito Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#55595c;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:600;line-height:1.2;color:#1a1a1a}.h1,h1{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h1,h1{font-size:2rem}}.h2,h2{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h2,h2{font-size:1.75rem}}.h3,h3{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h3,h3{font-size:1.5rem}}.h4,h4{font-size:1.25rem}.h5,h5{font-size:1rem}.h6,h6{font-size:.75rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#1a1a1a;text-decoration:underline}a:hover{color:#151515}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#1a1a1a}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#919aa1;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#919aa1}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #eceeef;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#919aa1}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{-ms-flex-negative:0;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-sm-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-sm-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-sm-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-sm-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-sm-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-sm-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-md-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-md-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-md-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-md-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-md-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-md-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-lg-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-lg-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-lg-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-lg-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-lg-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-lg-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-xl-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-xl-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-xl-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-xl-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-xl-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-xl-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{-ms-flex:1 0 0%;flex:1 0 0%}.row-cols-xxl-auto>*{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.row-cols-xxl-1>*{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.row-cols-xxl-2>*{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.row-cols-xxl-3>*{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.row-cols-xxl-4>*{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.row-cols-xxl-5>*{-ms-flex:0 0 auto;flex:0 0 auto;width:20%}.row-cols-xxl-6>*{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-xxl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-xxl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.333333%}.col-xxl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.666667%}.col-xxl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.col-xxl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.333333%}.col-xxl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.666667%}.col-xxl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.col-xxl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.333333%}.col-xxl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.666667%}.col-xxl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.col-xxl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.333333%}.col-xxl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.666667%}.col-xxl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.333333%}.offset-xxl-2{margin-left:16.666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.333333%}.offset-xxl-5{margin-left:41.666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.333333%}.offset-xxl-8{margin-left:66.666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.333333%}.offset-xxl-11{margin-left:91.666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#55595c;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#55595c;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#55595c;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#55595c;vertical-align:top;border-color:rgba(0,0,0,.05)}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#d1d1d1;--bs-table-striped-bg:#c7c7c7;--bs-table-striped-color:#000;--bs-table-active-bg:#bcbcbc;--bs-table-active-color:#000;--bs-table-hover-bg:#c1c1c1;--bs-table-hover-color:#000;color:#000;border-color:#bcbcbc}.table-secondary{--bs-table-bg:white;--bs-table-striped-bg:#f2f2f2;--bs-table-striped-color:#000;--bs-table-active-bg:#e6e6e6;--bs-table-active-color:#000;--bs-table-hover-bg:#ececec;--bs-table-hover-color:#000;color:#000;border-color:#e6e6e6}.table-success{--bs-table-bg:#dbf2e3;--bs-table-striped-bg:#d0e6d8;--bs-table-striped-color:#000;--bs-table-active-bg:#c5dacc;--bs-table-active-color:#000;--bs-table-hover-bg:#cbe0d2;--bs-table-hover-color:#000;color:#000;border-color:#c5dacc}.table-info{--bs-table-bg:#d2ebf5;--bs-table-striped-bg:#c8dfe9;--bs-table-striped-color:#000;--bs-table-active-bg:#bdd4dd;--bs-table-active-color:#000;--bs-table-hover-bg:#c2d9e3;--bs-table-hover-color:#000;color:#000;border-color:#bdd4dd}.table-warning{--bs-table-bg:#fcefdc;--bs-table-striped-bg:#efe3d1;--bs-table-striped-color:#000;--bs-table-active-bg:#e3d7c6;--bs-table-active-color:#000;--bs-table-hover-bg:#e9ddcc;--bs-table-hover-color:#000;color:#000;border-color:#e3d7c6}.table-danger{--bs-table-bg:#f7dddc;--bs-table-striped-bg:#ebd2d1;--bs-table-striped-color:#000;--bs-table-active-bg:#dec7c6;--bs-table-active-color:#000;--bs-table-hover-bg:#e4cccc;--bs-table-hover-color:#000;color:#000;border-color:#dec7c6}.table-light{--bs-table-bg:#fff;--bs-table-striped-bg:#f2f2f2;--bs-table-striped-color:#000;--bs-table-active-bg:#e6e6e6;--bs-table-active-color:#000;--bs-table-hover-bg:#ececec;--bs-table-hover-color:#000;color:#000;border-color:#e6e6e6}.table-dark{--bs-table-bg:#343a40;--bs-table-striped-bg:#3e444a;--bs-table-striped-color:#fff;--bs-table-active-bg:#484e53;--bs-table-active-color:#fff;--bs-table-hover-bg:#43494e;--bs-table-hover-color:#fff;color:#fff;border-color:#484e53}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:.75rem;padding-bottom:.75rem;margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:2rem;padding-bottom:2rem;font-size:1.25rem}.col-form-label-sm{padding-top:.5rem;padding-bottom:.5rem;font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#919aa1}.form-control{display:block;width:100%;padding:.75rem 1.5rem;font-size:1rem;font-weight:400;line-height:1.5;color:#55595c;background-color:#f7f7f9;background-clip:padding-box;border:0 solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#55595c;background-color:#f7f7f9;border-color:#8d8d8d;outline:0;box-shadow:0 0 0 .25rem rgba(26,26,26,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-webkit-input-placeholder{color:#919aa1;opacity:1}.form-control::-moz-placeholder{color:#919aa1;opacity:1}.form-control:-ms-input-placeholder{color:#919aa1;opacity:1}.form-control::-ms-input-placeholder{color:#919aa1;opacity:1}.form-control::placeholder{color:#919aa1;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#eceeef;opacity:1}.form-control::file-selector-button{padding:.75rem 1.5rem;margin:-.75rem -1.5rem;-webkit-margin-end:1.5rem;-moz-margin-end:1.5rem;margin-inline-end:1.5rem;color:#55595c;background-color:#eceeef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:0;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#e0e2e3}.form-control::-webkit-file-upload-button{padding:.75rem 1.5rem;margin:-.75rem -1.5rem;-webkit-margin-end:1.5rem;margin-inline-end:1.5rem;color:#55595c;background-color:#eceeef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:0;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#e0e2e3}.form-control-plaintext{display:block;width:100%;padding:.75rem 0;margin-bottom:0;line-height:1.5;color:#55595c;background-color:transparent;border:solid transparent;border-width:0 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + 1rem);padding:.5rem 1rem;font-size:.875rem}.form-control-sm::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;-moz-margin-end:1rem;margin-inline-end:1rem}.form-control-sm::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg{min-height:calc(1.5em + 4rem);padding:2rem 2rem;font-size:1.25rem}.form-control-lg::file-selector-button{padding:2rem 2rem;margin:-2rem -2rem;-webkit-margin-end:2rem;-moz-margin-end:2rem;margin-inline-end:2rem}.form-control-lg::-webkit-file-upload-button{padding:2rem 2rem;margin:-2rem -2rem;-webkit-margin-end:2rem;margin-inline-end:2rem}textarea.form-control{min-height:calc(1.5em + 1.5rem)}textarea.form-control-sm{min-height:calc(1.5em + 1rem)}textarea.form-control-lg{min-height:calc(1.5em + 4rem)}.form-control-color{width:3rem;height:auto;padding:.75rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em}.form-control-color::-webkit-color-swatch{height:1.5em}.form-select{display:block;width:100%;padding:.75rem 4.5rem .75rem 1.5rem;-moz-padding-start:calc(1.5rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#55595c;background-color:#f7f7f9;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right 1.5rem center;background-size:16px 12px;border:0 solid #ced4da;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#8d8d8d;outline:0;box-shadow:0 0 0 .25rem rgba(26,26,26,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:1.5rem;background-image:none}.form-select:disabled{background-color:#f7f7f9}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #55595c}.form-select-sm{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:.875rem}.form-select-lg{padding-top:2rem;padding-bottom:2rem;padding-left:2rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#f7f7f9;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{-webkit-filter:brightness(90%);filter:brightness(90%)}.form-check-input:focus{border-color:#8d8d8d;outline:0;box-shadow:0 0 0 .25rem rgba(26,26,26,.25)}.form-check-input:checked{background-color:#1a1a1a;border-color:#1a1a1a}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#1a1a1a;border-color:#1a1a1a;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;-webkit-filter:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%238d8d8d'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;-webkit-filter:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(26,26,26,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(26,26,26,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#1a1a1a;border:0;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#bababa}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#eceeef;border-color:transparent}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#1a1a1a;border:0;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#bababa}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#eceeef;border-color:transparent}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:3.5rem;line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem 1.5rem;pointer-events:none;border:0 solid transparent;-webkit-transform-origin:0 0;transform-origin:0 0;transition:opacity .1s ease-in-out,-webkit-transform .1s ease-in-out;transition:opacity .1s ease-in-out,transform .1s ease-in-out;transition:opacity .1s ease-in-out,transform .1s ease-in-out,-webkit-transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem 1.5rem}.form-floating>.form-control::-webkit-input-placeholder{color:transparent}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control:-ms-input-placeholder{color:transparent}.form-floating>.form-control::-ms-input-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-ms-input-placeholder){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-ms-input-placeholder)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;-webkit-transform:scale(.85) translateY(-.5rem) translateX(.15rem);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;-webkit-transform:scale(.85) translateY(-.5rem) translateX(.15rem);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.75rem 1.5rem;font-size:1rem;font-weight:400;line-height:1.5;color:#55595c;text-align:center;white-space:nowrap;background-color:#eceeef;border:0 solid #ced4da}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:2rem 2rem;font-size:1.25rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.5rem 1rem;font-size:.875rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:6rem}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#4bbf73}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(75,191,115,.9)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#4bbf73;padding-right:calc(1.5em + 1.5rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%234bbf73' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .375rem) center;background-size:calc(.75em + .75rem) calc(.75em + .75rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#4bbf73;box-shadow:0 0 0 .25rem rgba(75,191,115,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 1.5rem);background-position:top calc(.375em + .375rem) right calc(.375em + .375rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#4bbf73}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:8.25rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%234bbf73' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right 1.5rem center,center right 4.5rem;background-size:16px 12px,calc(.75em + .75rem) calc(.75em + .75rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#4bbf73;box-shadow:0 0 0 .25rem rgba(75,191,115,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#4bbf73}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#4bbf73}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(75,191,115,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#4bbf73}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#d9534f}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(217,83,79,.9)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#d9534f;padding-right:calc(1.5em + 1.5rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23d9534f'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23d9534f' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .375rem) center;background-size:calc(.75em + .75rem) calc(.75em + .75rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#d9534f;box-shadow:0 0 0 .25rem rgba(217,83,79,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 1.5rem);background-position:top calc(.375em + .375rem) right calc(.375em + .375rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#d9534f}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:8.25rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23d9534f'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23d9534f' stroke='none'/%3e%3c/svg%3e");background-position:right 1.5rem center,center right 4.5rem;background-size:16px 12px,calc(.75em + .75rem) calc(.75em + .75rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#d9534f;box-shadow:0 0 0 .25rem rgba(217,83,79,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#d9534f}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#d9534f}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(217,83,79,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#d9534f}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:600;line-height:1.5rem;color:#55595c;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:0 solid transparent;padding:.75rem 1.5rem;font-size:1rem;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#55595c}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(26,26,26,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#1a1a1a;border-color:#1a1a1a}.btn-primary:hover{color:#fff;background-color:#161616;border-color:#151515}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#161616;border-color:#151515;box-shadow:0 0 0 .25rem rgba(60,60,60,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#151515;border-color:#141414}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,60,60,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#1a1a1a;border-color:#1a1a1a}.btn-secondary{color:#000;background-color:#fff;border-color:#fff}.btn-secondary:hover{color:#000;background-color:#fff;border-color:#fff}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#000;background-color:#fff;border-color:#fff;box-shadow:0 0 0 .25rem rgba(217,217,217,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#000;background-color:#fff;border-color:#fff}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,217,217,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#000;background-color:#fff;border-color:#fff}.btn-success{color:#fff;background-color:#4bbf73;border-color:#4bbf73}.btn-success:hover{color:#fff;background-color:#40a262;border-color:#3c995c}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#40a262;border-color:#3c995c;box-shadow:0 0 0 .25rem rgba(102,201,136,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#3c995c;border-color:#388f56}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(102,201,136,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#4bbf73;border-color:#4bbf73}.btn-info{color:#fff;background-color:#1f9bcf;border-color:#1f9bcf}.btn-info:hover{color:#fff;background-color:#1a84b0;border-color:#197ca6}.btn-check:focus+.btn-info,.btn-info:focus{color:#fff;background-color:#1a84b0;border-color:#197ca6;box-shadow:0 0 0 .25rem rgba(65,170,214,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#197ca6;border-color:#17749b}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(65,170,214,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#1f9bcf;border-color:#1f9bcf}.btn-warning{color:#000;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning:hover{color:#000;background-color:#f2b969;border-color:#f2b560}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#f2b969;border-color:#f2b560;box-shadow:0 0 0 .25rem rgba(204,147,66,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#f3bd71;border-color:#f2b560}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(204,147,66,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#f0ad4e;border-color:#f0ad4e}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger:hover{color:#fff;background-color:#b84743;border-color:#ae423f}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#b84743;border-color:#ae423f;box-shadow:0 0 0 .25rem rgba(223,109,105,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#ae423f;border-color:#a33e3b}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(223,109,105,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-light{color:#000;background-color:#fff;border-color:#fff}.btn-light:hover{color:#000;background-color:#fff;border-color:#fff}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#fff;border-color:#fff;box-shadow:0 0 0 .25rem rgba(217,217,217,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#fff;border-color:#fff}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,217,217,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#fff;border-color:#fff}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#2c3136;border-color:#2a2e33}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#2c3136;border-color:#2a2e33;box-shadow:0 0 0 .25rem rgba(82,88,93,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#2a2e33;border-color:#272c30}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-primary{color:#1a1a1a;border-color:#1a1a1a}.btn-outline-primary:hover{color:#fff;background-color:#1a1a1a;border-color:#1a1a1a}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(26,26,26,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#1a1a1a;border-color:#1a1a1a}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(26,26,26,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#1a1a1a;background-color:transparent}.btn-outline-secondary{color:#fff;border-color:#fff}.btn-outline-secondary:hover{color:#000;background-color:#fff;border-color:#fff}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(255,255,255,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#000;background-color:#fff;border-color:#fff}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(255,255,255,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#fff;background-color:transparent}.btn-outline-success{color:#4bbf73;border-color:#4bbf73}.btn-outline-success:hover{color:#fff;background-color:#4bbf73;border-color:#4bbf73}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(75,191,115,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#4bbf73;border-color:#4bbf73}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(75,191,115,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#4bbf73;background-color:transparent}.btn-outline-info{color:#1f9bcf;border-color:#1f9bcf}.btn-outline-info:hover{color:#fff;background-color:#1f9bcf;border-color:#1f9bcf}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(31,155,207,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#fff;background-color:#1f9bcf;border-color:#1f9bcf}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(31,155,207,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#1f9bcf;background-color:transparent}.btn-outline-warning{color:#f0ad4e;border-color:#f0ad4e}.btn-outline-warning:hover{color:#000;background-color:#f0ad4e;border-color:#f0ad4e}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(240,173,78,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#f0ad4e;border-color:#f0ad4e}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(240,173,78,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#f0ad4e;background-color:transparent}.btn-outline-danger{color:#d9534f;border-color:#d9534f}.btn-outline-danger:hover{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(217,83,79,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(217,83,79,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#d9534f;background-color:transparent}.btn-outline-light{color:#fff;border-color:#fff}.btn-outline-light:hover{color:#000;background-color:#fff;border-color:#fff}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(255,255,255,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#fff;border-color:#fff}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(255,255,255,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#fff;background-color:transparent}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(52,58,64,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#343a40;border-color:#343a40}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-link{font-weight:400;color:#1a1a1a;text-decoration:underline}.btn-link:hover{color:#151515}.btn-link.disabled,.btn-link:disabled{color:#919aa1}.btn-group-lg>.btn,.btn-lg{padding:2rem 2rem;font-size:1.25rem;border-radius:0}.btn-group-sm>.btn,.btn-sm{padding:.5rem 1rem;font-size:.875rem;border-radius:0}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#55595c;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#1a1a1a;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#171717;background-color:#f7f7f9}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#1a1a1a}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#919aa1;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#1a1a1a}.dropdown-menu-dark{color:#eceeef;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#eceeef}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#1a1a1a}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#eceeef}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:0}.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:1.5rem;padding-left:1.5rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:0}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#1a1a1a;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#151515}.nav-link.disabled{color:#919aa1;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #eceeef}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#f7f7f9 #f7f7f9 #eceeef;isolation:isolate}.nav-tabs .nav-link.disabled{color:#919aa1;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#55595c;background-color:#fff;border-color:#eceeef #eceeef #fff}.nav-tabs .dropdown-menu{margin-top:-1px}.nav-pills .nav-link{background:0 0;border:0}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#1a1a1a}.nav-fill .nav-item,.nav-fill>.nav-link{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding-top:1.5rem;padding-bottom:1.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:-ms-flexbox;display:flex;-ms-flex-wrap:inherit;flex-wrap:inherit;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;-ms-flex-positive:1;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;-webkit-transform:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:-ms-flexbox;display:flex;-ms-flex-positive:0;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:#1a1a1a}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:#1a1a1a}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:#1a1a1a}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:#1a1a1a}.navbar-light .navbar-toggler{color:rgba(0,0,0,.3);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.3%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.3)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:#1a1a1a}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:#fff}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0}.card>.list-group:last-child{border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}}.accordion-button{position:relative;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#55595c;text-align:left;background-color:#fff;border:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#171717;background-color:#e8e8e8;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23171717'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.accordion-button::after{-ms-flex-negative:0;flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2355595c'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out;transition:transform .2s ease-in-out,-webkit-transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#8d8d8d;outline:0;box-shadow:0 0 0 .25rem rgba(26,26,26,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:not(:first-of-type){border-top:0}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#919aa1;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#919aa1}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#1a1a1a;text-decoration:none;background-color:#fff;border:1px solid transparent;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#151515;background-color:#f7f7f9;border-color:transparent}.page-link:focus{z-index:3;color:#151515;background-color:#f7f7f9;outline:0;box-shadow:0 0 0 .25rem rgba(26,26,26,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#1a1a1a;border-color:#1a1a1a}.page-item.disabled .page-link{color:#919aa1;pointer-events:none;background-color:#fff;border-color:transparent}.page-link{padding:.375rem .75rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#101010;background-color:#d1d1d1;border-color:#bababa}.alert-primary .alert-link{color:#0d0d0d}.alert-secondary{color:#999;background-color:#fff;border-color:#fff}.alert-secondary .alert-link{color:#7a7a7a}.alert-success{color:#2d7345;background-color:#dbf2e3;border-color:#c9ecd5}.alert-success .alert-link{color:#245c37}.alert-info{color:#135d7c;background-color:#d2ebf5;border-color:#bce1f1}.alert-info .alert-link{color:#0f4a63}.alert-warning{color:#90682f;background-color:#fcefdc;border-color:#fbe6ca}.alert-warning .alert-link{color:#735326}.alert-danger{color:#82322f;background-color:#f7dddc;border-color:#f4cbca}.alert-danger .alert-link{color:#682826}.alert-light{color:#999;background-color:#fff;border-color:#fff}.alert-light .alert-link{color:#7a7a7a}.alert-dark{color:#1f2326;background-color:#d6d8d9;border-color:#c2c4c6}.alert-dark .alert-link{color:#191c1e}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#f7f7f9}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#1a1a1a;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#55595c;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#55595c;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#55595c;background-color:#f7f7f9}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#1a1a1a;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item.disabled,.list-group-item:disabled{color:#919aa1;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#1a1a1a;border-color:#1a1a1a}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#101010;background-color:#d1d1d1}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#101010;background-color:#bcbcbc}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#101010;border-color:#101010}.list-group-item-secondary{color:#999;background-color:#fff}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#999;background-color:#e6e6e6}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#999;border-color:#999}.list-group-item-success{color:#2d7345;background-color:#dbf2e3}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#2d7345;background-color:#c5dacc}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#2d7345;border-color:#2d7345}.list-group-item-info{color:#135d7c;background-color:#d2ebf5}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#135d7c;background-color:#bdd4dd}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#135d7c;border-color:#135d7c}.list-group-item-warning{color:#90682f;background-color:#fcefdc}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#90682f;background-color:#e3d7c6}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#90682f;border-color:#90682f}.list-group-item-danger{color:#82322f;background-color:#f7dddc}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#82322f;background-color:#dec7c6}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#82322f;border-color:#82322f}.list-group-item-light{color:#999;background-color:#fff}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#999;background-color:#e6e6e6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#999;border-color:#999}.list-group-item-dark{color:#1f2326;background-color:#d6d8d9}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1f2326;background-color:#c1c2c3}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1f2326;border-color:#1f2326}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(26,26,26,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;opacity:.25}.btn-close-white{-webkit-filter:invert(1) grayscale(100%) brightness(200%);filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.5rem .75rem;color:#919aa1;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-negative:0;flex-shrink:0;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #eceeef}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-negative:0;flex-shrink:0;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #eceeef}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2)}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;color:#1a1a1a;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#55595c}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{-webkit-filter:invert(1) grayscale(100);filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{-ms-flex-positive:1;flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);-webkit-transform:translateX(-100%);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);-webkit-transform:translateX(100%);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);-webkit-transform:translateY(-100%);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);-webkit-transform:translateY(100%);transform:translateY(100%)}.offcanvas.show{-webkit-transform:none;transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0;mask-position:-200% 0}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0;mask-position:-200% 0}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#1a1a1a}.link-primary:focus,.link-primary:hover{color:#151515}.link-secondary{color:#fff}.link-secondary:focus,.link-secondary:hover{color:#fff}.link-success{color:#4bbf73}.link-success:focus,.link-success:hover{color:#3c995c}.link-info{color:#1f9bcf}.link-info:focus,.link-info:hover{color:#197ca6}.link-warning{color:#f0ad4e}.link-warning:focus,.link-warning:hover{color:#f3bd71}.link-danger{color:#d9534f}.link-danger:focus,.link-danger:hover{color:#ae423f}.link-light{color:#fff}.link-light:focus,.link-light:hover{color:#fff}.link-dark{color:#343a40}.link-dark:focus,.link-dark:hover{color:#2a2e33}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-align:center;align-items:center;-ms-flex-item-align:stretch;align-self:stretch}.vstack{display:-ms-flexbox;display:flex;-ms-flex:1 1 auto;flex:1 1 auto;-ms-flex-direction:column;flex-direction:column;-ms-flex-item-align:stretch;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;-ms-flex-item-align:stretch;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{-webkit-transform:translate(-50%,-50%)!important;transform:translate(-50%,-50%)!important}.translate-middle-x{-webkit-transform:translateX(-50%)!important;transform:translateX(-50%)!important}.translate-middle-y{-webkit-transform:translateY(-50%)!important;transform:translateY(-50%)!important}.border{border:1px solid #eceeef!important}.border-0{border:0!important}.border-top{border-top:1px solid #eceeef!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #eceeef!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #eceeef!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #eceeef!important}.border-start-0{border-left:0!important}.border-primary{border-color:#1a1a1a!important}.border-secondary{border-color:#fff!important}.border-success{border-color:#4bbf73!important}.border-info{border-color:#1f9bcf!important}.border-warning{border-color:#f0ad4e!important}.border-danger{border-color:#d9534f!important}.border-light{border-color:#fff!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-first{-ms-flex-order:-1!important;order:-1!important}.order-0{-ms-flex-order:0!important;order:0!important}.order-1{-ms-flex-order:1!important;order:1!important}.order-2{-ms-flex-order:2!important;order:2!important}.order-3{-ms-flex-order:3!important;order:3!important}.order-4{-ms-flex-order:4!important;order:4!important}.order-5{-ms-flex-order:5!important;order:5!important}.order-last{-ms-flex-order:6!important;order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.325rem + .9vw)!important}.fs-2{font-size:calc(1.3rem + .6vw)!important}.fs-3{font-size:calc(1.275rem + .3vw)!important}.fs-4{font-size:1.25rem!important}.fs-5{font-size:1rem!important}.fs-6{font-size:.75rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#919aa1!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-sm-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-sm-first{-ms-flex-order:-1!important;order:-1!important}.order-sm-0{-ms-flex-order:0!important;order:0!important}.order-sm-1{-ms-flex-order:1!important;order:1!important}.order-sm-2{-ms-flex-order:2!important;order:2!important}.order-sm-3{-ms-flex-order:3!important;order:3!important}.order-sm-4{-ms-flex-order:4!important;order:4!important}.order-sm-5{-ms-flex-order:5!important;order:5!important}.order-sm-last{-ms-flex-order:6!important;order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-md-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-md-first{-ms-flex-order:-1!important;order:-1!important}.order-md-0{-ms-flex-order:0!important;order:0!important}.order-md-1{-ms-flex-order:1!important;order:1!important}.order-md-2{-ms-flex-order:2!important;order:2!important}.order-md-3{-ms-flex-order:3!important;order:3!important}.order-md-4{-ms-flex-order:4!important;order:4!important}.order-md-5{-ms-flex-order:5!important;order:5!important}.order-md-last{-ms-flex-order:6!important;order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-lg-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-lg-first{-ms-flex-order:-1!important;order:-1!important}.order-lg-0{-ms-flex-order:0!important;order:0!important}.order-lg-1{-ms-flex-order:1!important;order:1!important}.order-lg-2{-ms-flex-order:2!important;order:2!important}.order-lg-3{-ms-flex-order:3!important;order:3!important}.order-lg-4{-ms-flex-order:4!important;order:4!important}.order-lg-5{-ms-flex-order:5!important;order:5!important}.order-lg-last{-ms-flex-order:6!important;order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-xl-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-xl-first{-ms-flex-order:-1!important;order:-1!important}.order-xl-0{-ms-flex-order:0!important;order:0!important}.order-xl-1{-ms-flex-order:1!important;order:1!important}.order-xl-2{-ms-flex-order:2!important;order:2!important}.order-xl-3{-ms-flex-order:3!important;order:3!important}.order-xl-4{-ms-flex-order:4!important;order:4!important}.order-xl-5{-ms-flex-order:5!important;order:5!important}.order-xl-last{-ms-flex-order:6!important;order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:-ms-flexbox!important;display:flex!important}.d-xxl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xxl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xxl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xxl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xxl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xxl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xxl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xxl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xxl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.flex-xxl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xxl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xxl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xxl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xxl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xxl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.justify-content-xxl-evenly{-ms-flex-pack:space-evenly!important;justify-content:space-evenly!important}.align-items-xxl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xxl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xxl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xxl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xxl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xxl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xxl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xxl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xxl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xxl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xxl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xxl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xxl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xxl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xxl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xxl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xxl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}.order-xxl-first{-ms-flex-order:-1!important;order:-1!important}.order-xxl-0{-ms-flex-order:0!important;order:0!important}.order-xxl-1{-ms-flex-order:1!important;order:1!important}.order-xxl-2{-ms-flex-order:2!important;order:2!important}.order-xxl-3{-ms-flex-order:3!important;order:3!important}.order-xxl-4{-ms-flex-order:4!important;order:4!important}.order-xxl-5{-ms-flex-order:5!important;order:5!important}.order-xxl-last{-ms-flex-order:6!important;order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2rem!important}.fs-2{font-size:1.75rem!important}.fs-3{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}.d-print-none{display:none!important}}.navbar{font-size:.875rem;text-transform:uppercase;font-weight:600}.navbar-nav .nav-link{padding-top:.715rem;padding-bottom:.715rem}.navbar-brand{margin-right:2rem}.bg-primary{background-color:theme-color("primary")!important}.bg-light{border:1px solid rgba(0,0,0,.1)}.bg-light.navbar-fixed-top{border-width:0 0 1px}.bg-light.navbar-bottom-top{border-width:1px 0 0}.nav-item{margin-right:2rem}.btn{font-size:.875rem;text-transform:uppercase}.btn-group-sm>.btn,.btn-sm{font-size:10px}.btn-warning,.btn-warning:focus,.btn-warning:hover,.btn-warning:not([disabled]):not(.disabled):active{color:#fff}.btn-outline-secondary{border-color:#919aa1;color:#919aa1}.btn-outline-secondary:not([disabled]):not(.disabled):active,.btn-outline-secondary:not([disabled]):not(.disabled):focus,.btn-outline-secondary:not([disabled]):not(.disabled):hover{background-color:#ced4da;border-color:#ced4da;color:#fff}.btn-outline-secondary:not([disabled]):not(.disabled):focus{box-shadow:0 0 0 .2rem rgba(206,212,218,.5)}[class*=btn-outline-]{border-width:2px}.border-secondary{border:1px solid #ced4da!important}body{font-weight:200;letter-spacing:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{text-transform:uppercase;letter-spacing:3px}.text-secondary{color:#55595c!important}th{font-size:.875rem;text-transform:uppercase}.table td,.table th{padding:1.5rem}.table-sm td,.table-sm th{padding:.75rem}.dropdown-menu{font-size:.875rem;text-transform:none}.badge{padding-top:.28rem}.badge-pill{border-radius:10rem}.badge.bg-light,.badge.bg-secondary{color:#343a40}.list-group-item .h1,.list-group-item .h2,.list-group-item .h3,.list-group-item .h4,.list-group-item .h5,.list-group-item .h6,.list-group-item h1,.list-group-item h2,.list-group-item h3,.list-group-item h4,.list-group-item h5,.list-group-item h6{color:inherit}.card-header,.card-title{color:inherit} \ No newline at end of file
diff --git a/resources/tools/dash/app/pal/static/dist/img/favicon.svg b/resources/tools/dash/app/pal/static/dist/img/favicon.svg
deleted file mode 100644
index 689757e3fd..0000000000
--- a/resources/tools/dash/app/pal/static/dist/img/favicon.svg
+++ /dev/null
@@ -1,348 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
- viewBox="0 0 1000 568.31" style="enable-background:new 0 0 1000 568.31;" xml:space="preserve">
-<style type="text/css">
- .st0{fill:#1DCAD3;}
- .st1{fill:#36B0C9;}
- .st2{fill:#231F20;}
- .st3{fill:#FFFFFF;}
- .st4{fill:#9164CC;}
- .st5{clip-path:url(#SVGID_2_);fill:url(#SVGID_3_);}
- .st6{fill:#201747;}
- .st7{fill-rule:evenodd;clip-rule:evenodd;fill:#10CFC9;}
- .st8{clip-path:url(#SVGID_5_);fill:#231F20;}
- .st9{fill-rule:evenodd;clip-rule:evenodd;fill:#231F20;}
- .st10{clip-path:url(#SVGID_7_);fill:#FFFFFF;}
- .st11{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
- .st12{fill:#8CCEAF;}
- .st13{fill:#008476;}
- .st14{fill:#25BCBD;}
- .st15{fill:#004D70;}
- .st16{fill:#20BBBB;}
- .st17{fill:#024D70;}
- .st18{fill-rule:evenodd;clip-rule:evenodd;fill:#F58B1F;}
- .st19{fill-rule:evenodd;clip-rule:evenodd;fill:#18335B;}
- .st20{clip-path:url(#SVGID_9_);}
- .st21{clip-path:url(#SVGID_11_);}
- .st22{fill:#18335B;}
- .st23{fill:#F58B1F;}
- .st24{clip-path:url(#SVGID_15_);}
- .st25{clip-path:url(#SVGID_17_);}
- .st26{clip-path:url(#SVGID_21_);}
- .st27{clip-path:url(#SVGID_23_);}
- .st28{clip-path:url(#SVGID_27_);}
- .st29{clip-path:url(#SVGID_29_);}
- .st30{clip-path:url(#SVGID_33_);}
- .st31{clip-path:url(#SVGID_35_);}
- .st32{clip-path:url(#SVGID_39_);}
- .st33{clip-path:url(#SVGID_41_);}
- .st34{fill:#416BA9;}
- .st35{fill:#73C3D5;}
- .st36{opacity:0.8;}
- .st37{fill:#3A3A3A;}
- .st38{fill:url(#SVGID_44_);}
- .st39{fill:none;stroke:#000000;stroke-width:6.3384;}
- .st40{fill:none;stroke:#000000;stroke-width:3.1692;}
- .st41{fill:#48494B;}
- .st42{fill:#C1986C;}
- .st43{fill:url(#SVGID_63_);}
- .st44{fill:url(#SVGID_64_);}
- .st45{fill:url(#SVGID_65_);}
- .st46{fill:url(#SVGID_66_);}
- .st47{fill:url(#SVGID_67_);}
- .st48{fill:#4D4E4E;}
- .st49{fill:#27B373;}
- .st50{fill:#5DC4CD;}
- .st51{fill:#1E8756;}
- .st52{fill:#3D1152;}
- .st53{fill:#922C48;}
- .st54{fill-rule:evenodd;clip-rule:evenodd;fill:#922C48;}
- .st55{fill:#404041;}
- .st56{fill:#EC1C24;}
- .st57{fill:#373A36;}
- .st58{fill:#808184;}
- .st59{fill:#262261;}
- .st60{fill:#6FCBDC;}
- .st61{fill:#2F3436;}
- .st62{fill:#5F97D0;}
- .st63{fill:#132428;}
- .st64{fill:#85C041;}
- .st65{fill:#677784;}
- .st66{fill:url(#SVGID_68_);}
- .st67{opacity:0.2;clip-path:url(#SVGID_70_);}
- .st68{fill:#FFFEFA;}
- .st69{opacity:0.1;}
- .st70{fill:url(#SVGID_71_);}
- .st71{opacity:0.3;}
- .st72{opacity:0.08;}
- .st73{opacity:0.1;fill:url(#Wordmark_1_);}
- .st74{fill:url(#SVGID_104_);}
- .st75{opacity:0.6;fill:url(#SVGID_107_);}
- .st76{opacity:0.4;}
- .st77{fill:url(#SVGID_110_);}
- .st78{opacity:0.6;fill:url(#SVGID_113_);}
- .st79{fill:url(#SVGID_116_);}
- .st80{opacity:0.6;fill:url(#SVGID_119_);}
- .st81{fill:url(#SVGID_122_);}
- .st82{opacity:0.6;fill:url(#SVGID_125_);}
- .st83{fill:url(#SVGID_128_);}
- .st84{opacity:0.6;fill:url(#SVGID_131_);}
- .st85{fill:#221F1F;}
- .st86{fill:none;}
- .st87{fill:#00416B;}
- .st88{opacity:0.8;fill:url(#XMLID_323_);}
- .st89{fill:#4197CB;}
- .st90{fill:#003E52;}
- .st91{fill:#3F96B4;}
- .st92{fill:#B9DBE5;}
- .st93{opacity:0.3;fill:#231F20;}
- .st94{opacity:0.3;fill:#FFFFFF;}
- .st95{fill:#050013;}
- .st96{fill:#E87200;}
- .st97{fill:#FCB813;}
- .st98{fill:#3D3935;}
- .st99{fill:#FFB600;}
- .st100{fill:#FCB814;}
- .st101{fill:#F48120;}
- .st102{fill:#EF4E25;}
- .st103{fill:#ED3024;}
- .st104{fill:#E0592A;}
- .st105{fill:#00ADBB;}
- .st106{fill:#00829B;}
- .st107{fill:#93D500;}
- .st108{fill:#4D5A31;}
- .st109{fill:#6BA43A;}
- .st110{fill:#424143;}
- .st111{fill-rule:evenodd;clip-rule:evenodd;fill:#C7E6B4;}
- .st112{fill-rule:evenodd;clip-rule:evenodd;fill:#5A9891;}
- .st113{fill-rule:evenodd;clip-rule:evenodd;fill:#127870;}
- .st114{fill-rule:evenodd;clip-rule:evenodd;fill:#5CCFD5;}
- .st115{fill-rule:evenodd;clip-rule:evenodd;fill:#ACD5CD;}
- .st116{fill-rule:evenodd;clip-rule:evenodd;fill:#B5ECC9;}
- .st117{fill-rule:evenodd;clip-rule:evenodd;fill:#A1D683;}
- .st118{fill-rule:evenodd;clip-rule:evenodd;fill:#DEF0D3;}
- .st119{fill-rule:evenodd;clip-rule:evenodd;fill:#91B9B4;}
- .st120{fill-rule:evenodd;clip-rule:evenodd;fill:#006860;}
- .st121{fill-rule:evenodd;clip-rule:evenodd;fill:#00ADBB;}
- .st122{fill-rule:evenodd;clip-rule:evenodd;fill:#B4E7E9;}
- .st123{fill-rule:evenodd;clip-rule:evenodd;fill:#007565;}
- .st124{fill-rule:evenodd;clip-rule:evenodd;fill:#00CE7C;}
- .st125{fill-rule:evenodd;clip-rule:evenodd;fill:#5FD896;}
- .st126{fill:#007DA5;}
- .st127{fill:#313032;}
- .st128{fill:#24272A;}
- .st129{fill:#00AFAA;}
- .st130{fill:#66C9BA;}
- .st131{fill:#0069A7;}
- .st132{fill:#002F87;}
- .st133{fill:#8BC53F;}
- .st134{fill:#1A1A1A;}
- .st135{fill:#0095D6;}
- .st136{fill:#003F5F;}
- .st137{fill:#2D317C;}
- .st138{fill:#41BFBF;}
- .st139{fill:#293C97;}
- .st140{fill:#52C2BD;}
- .st141{fill:url(#SVGID_134_);}
- .st142{fill:url(#SVGID_135_);}
- .st143{fill:url(#SVGID_136_);}
- .st144{fill:#0DBEEA;}
- .st145{fill:#097EC2;}
- .st146{fill:#133C63;}
- .st147{fill:#3B91CF;}
- .st148{fill:#C8DEE8;}
- .st149{fill:#629BBA;}
- .st150{fill:#F8BE19;}
- .st151{fill:url(#SVGID_137_);}
- .st152{fill:url(#SVGID_138_);}
- .st153{fill:url(#SVGID_139_);}
- .st154{fill:#00233B;}
- .st155{fill:url(#SVGID_140_);}
- .st156{fill:url(#SVGID_141_);}
- .st157{fill:url(#SVGID_142_);}
- .st158{fill:url(#SVGID_143_);}
- .st159{fill:url(#SVGID_144_);}
- .st160{fill:url(#SVGID_145_);}
- .st161{fill:url(#SVGID_146_);}
- .st162{fill:url(#SVGID_147_);}
- .st163{fill:url(#SVGID_148_);}
- .st164{fill:url(#SVGID_149_);}
- .st165{fill:url(#SVGID_150_);}
- .st166{fill:url(#SVGID_151_);}
- .st167{fill:url(#SVGID_152_);}
- .st168{fill:url(#SVGID_153_);}
- .st169{fill:url(#SVGID_154_);}
- .st170{fill:url(#SVGID_155_);}
- .st171{fill:url(#SVGID_156_);}
- .st172{fill:url(#SVGID_157_);}
- .st173{fill:url(#SVGID_158_);}
- .st174{fill:url(#SVGID_159_);}
- .st175{fill:url(#SVGID_160_);}
- .st176{fill:url(#SVGID_161_);}
- .st177{fill:url(#SVGID_162_);}
- .st178{fill:url(#SVGID_163_);}
- .st179{fill:url(#SVGID_164_);}
- .st180{fill:url(#SVGID_165_);}
- .st181{fill:url(#SVGID_166_);}
- .st182{fill:url(#SVGID_167_);}
- .st183{fill:url(#SVGID_168_);}
- .st184{fill:url(#SVGID_169_);}
- .st185{fill:url(#SVGID_170_);}
- .st186{fill:url(#SVGID_171_);}
- .st187{fill:url(#SVGID_172_);}
- .st188{fill:url(#SVGID_173_);}
- .st189{fill:url(#SVGID_174_);}
- .st190{fill:url(#SVGID_175_);}
- .st191{fill:url(#SVGID_176_);}
- .st192{fill:url(#SVGID_177_);}
- .st193{fill:url(#SVGID_178_);}
- .st194{fill:#C31230;}
- .st195{fill:#807F82;}
- .st196{fill-rule:evenodd;clip-rule:evenodd;fill:#C31230;}
- .st197{fill-rule:evenodd;clip-rule:evenodd;fill:#807F82;}
- .st198{fill:#2D2D2D;}
- .st199{display:none;fill:#2D2D2D;}
- .st200{fill:#D11F3C;}
- .st201{fill:#E42C4C;stroke:#E42C4C;stroke-width:1.0503;stroke-miterlimit:10;}
- .st202{display:none;fill:#231F20;}
- .st203{display:none;fill:#FFFFFF;}
- .st204{fill:#FF7F30;}
- .st205{opacity:0.3;fill:#FF7F30;}
- .st206{opacity:0.6;fill:#FF7F30;}
- .st207{opacity:0.7;fill:#FF7F30;}
- .st208{fill:#221C35;}
- .st209{fill:#1B98D5;}
- .st210{fill:#173963;}
- .st211{fill:#009ADE;}
- .st212{fill:#003764;}
- .st213{fill:#2A7DE1;}
- .st214{opacity:0.4;clip-path:url(#XMLID_324_);fill:#221F1F;}
- .st215{fill:#002A3A;}
- .st216{fill:#0033A1;}
- .st217{fill:url(#SVGID_179_);}
- .st218{fill:url(#SVGID_180_);}
- .st219{fill:url(#SVGID_181_);}
- .st220{fill:url(#SVGID_182_);}
- .st221{fill:#007EC4;}
- .st222{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_183_);}
- .st223{fill-rule:evenodd;clip-rule:evenodd;fill:#E6E7E8;}
- .st224{fill:#009345;}
- .st225{fill:#BBBCB8;}
- .st226{fill:#72C0EB;}
- .st227{fill:#939598;}
- .st228{fill-rule:evenodd;clip-rule:evenodd;fill:#2CB8EB;}
- .st229{fill:#2CB8EB;}
- .st230{fill:#81B83A;}
- .st231{fill-rule:evenodd;clip-rule:evenodd;fill:#81B83A;}
- .st232{enable-background:new ;}
- .st233{fill:#FF6F3E;}
- .st234{fill:#12143D;}
- .st235{fill:url(#SVGID_184_);}
- .st236{fill:url(#SVGID_185_);}
- .st237{fill:url(#SVGID_186_);}
- .st238{fill:url(#SVGID_187_);}
- .st239{fill:url(#SVGID_188_);}
- .st240{fill:url(#SVGID_189_);}
- .st241{fill:url(#SVGID_190_);}
- .st242{fill:url(#SVGID_191_);}
- .st243{fill:url(#SVGID_192_);}
- .st244{fill:#7C51A0;}
- .st245{fill:#9F66A9;}
- .st246{fill:#9F80B9;}
- .st247{fill:url(#SVGID_193_);}
- .st248{fill:url(#SVGID_194_);}
- .st249{fill:url(#SVGID_195_);}
- .st250{fill:url(#SVGID_196_);}
- .st251{fill:#2D3136;}
- .st252{fill:#76777A;}
- .st253{fill:#A7A8A9;}
- .st254{fill:#0082CA;}
- .st255{fill:#FFB259;}
- .st256{fill:#385CAD;}
- .st257{fill:#7BA0C4;}
- .st258{fill:#EBA900;}
- .st259{fill:#929497;}
- .st260{opacity:0.7;fill:#FFFFFF;}
- .st261{fill:#016BAF;}
- .st262{fill:#343432;}
- .st263{fill:#6D6E70;}
- .st264{fill:#F4B01B;}
- .st265{fill:#293271;}
- .st266{fill:#A1D33C;}
- .st267{fill:#212322;}
- .st268{fill:#0047BA;}
- .st269{fill:#969CDE;}
- .st270{fill:#047BC1;}
- .st271{fill:url(#SVGID_197_);}
- .st272{fill:url(#SVGID_198_);}
- .st273{fill:url(#SVGID_199_);}
- .st274{fill:url(#SVGID_200_);}
- .st275{fill:url(#SVGID_201_);}
- .st276{fill:url(#SVGID_202_);}
- .st277{fill:url(#SVGID_203_);}
- .st278{fill:#13517C;}
- .st279{fill:#0077A6;}
- .st280{fill:none;stroke:#231F20;stroke-width:5.9036;stroke-miterlimit:10;}
- .st281{fill:#00A94F;}
- .st282{fill:none;stroke:#231F20;stroke-width:3.2172;stroke-miterlimit:10;}
- .st283{fill:#59595C;}
- .st284{opacity:0.349;fill:#F9AE19;}
- .st285{opacity:0.349;fill:#E99F22;}
- .st286{opacity:0.349;fill:#E47D25;}
- .st287{fill:#F9AE19;}
- .st288{fill:#E99F22;}
- .st289{fill:#F09B20;}
- .st290{fill:#E47D25;}
- .st291{fill:#E89223;}
- .st292{opacity:0.651;fill:#F9AE19;}
- .st293{fill:#E68825;}
- .st294{opacity:0.651;fill:#E99F22;}
- .st295{fill:#EB8D23;}
- .st296{opacity:0.7725;fill:#EF9B21;}
- .st297{opacity:0.651;fill:#E47D25;}
- .st298{opacity:0.7725;fill:#EA9622;}
- .st299{fill:url(#SVGID_204_);}
- .st300{fill:#55575B;}
- .st301{fill:#EE424E;}
- .st302{fill:#34424B;}
-</style>
-<g>
- <g>
- <path class="st55" d="M772.88,526c9.95,0,15.7,5.53,15.7,15.48c0,10.17-5.75,15.48-15.7,15.48c-9.95,0-15.48-5.31-15.48-15.48
- C757.4,531.53,762.93,526,772.88,526z"/>
- <path class="st55" d="M832.94,393.35c8.18,0,13.71,3.32,13.71,12.38c0,9.29-5.53,12.6-13.71,12.6c-8.4,0-14.15-3.32-14.15-12.6
- C818.79,396.67,824.54,393.35,832.94,393.35z M821.22,438.67h22.99V554.3h-22.99V438.67z"/>
- <path class="st55" d="M934.56,435.58c36.25,0,61.9,26.09,61.9,61.24c0,34.71-25.65,60.58-61.9,60.58
- c-35.82,0-61.69-25.87-61.69-60.58C872.88,461.67,898.75,435.58,934.56,435.58z M934.56,536.18c23.66,0,39.79-17.03,39.79-39.36
- c0-22.77-16.14-40.02-39.79-40.02c-23.44,0-39.36,17.25-39.36,40.02C895.21,519.15,911.13,536.18,934.56,536.18z"/>
- </g>
- <g>
- <path class="st56" d="M724.15,245.36c-0.97-8.4-17.24-16.23-17.24-16.23c-6.81-4.71-8.03-16.23-7.16-20.6
- c0.87-4.36,15.01-36.66,15.01-36.66c11.52-11.35,31.6-35.44,33.87-48.88c1.07-6.31-3.14-38.93,5.41-49.75
- c5.63-7.12,22.35-15.36,25.84-18.16c3.49-2.79,6.28-11,4.71-14.84c-1.57-3.84-27.41-4.02-27.41-4.02s-2.16-10.79-17.62-11.8
- c-6.05-0.2-10.18,1.3-10.18,1.3c1.78-0.89,4.69-2.18,6.78-3.1c-0.54-3.56-1.89-11.45-3.24-11.72c-1.43-0.29-4.73,5.28-6.27,7.21
- l0.28,3.42l-1.11-2.85l-1.63-4.2l-0.02-0.06h0c-0.64-1.74-1.36-3.37-2.08-3.53c-1.57-0.34-38.06,50.81-42.6,57.44
- c-4.54,6.63-9.25,17.81-33.87,25.49c-17.37,5.42-53.43,6.32-81.13,15.16l0,0c-14.25,1.43-53.29,33.79-83.15,32.62
- c-20.05-0.78-35.61-6.28-39.57-15.25c0,0,4.19,41.32,25.6,43.64c0,0,13.5,2.33,26.53-9.31c0,0-3.72,7.36-8.38,9.5
- c0,0,18.07-4.62,29.06-12.94c3.37-7.06,9.22-17.4,17.46-25.17c0.53-0.52,0.86-0.8,0.86-0.8c-0.29,0.26-0.57,0.53-0.86,0.8
- c-2.65,2.6-10.68,11.48-11.86,24.73c-0.24,2.64-0.21,5.44,0.2,8.41c0,8.9,9.66,20.78,9.66,26.19c0,7.16-16.76,20.25-17.81,23.74
- c-0.58,1.95-2.35,16.91,0.69,29.77h-59.09c-56.8,0-78.68,56.8-78.68,56.8l-24.4,68h-10.52c-8.25,0-14.94,6.69-14.94,14.94
- c0,8.19,6.59,14.81,14.75,14.92l-0.01,0.02h0.2h5.92c8.25,0,14.94,6.69,14.94,14.94c0,8.25-6.69,14.94-14.94,14.94H197.01
- c-8.25,0-14.94,6.69-14.94,14.94c0,8.25,6.69,14.94,14.94,14.94h154.15c8.25,0,14.94,6.69,14.94,14.94
- c0,8.25-6.69,14.94-14.94,14.94h-32.39H146.48h-32.39c-8.25,0-14.94,6.69-14.94,14.94c0,8.25,6.69,14.94,14.94,14.94h183.08
- l-13.71,38.21h113.6l35.67-99.4h1.72c79.83,0,109.91-85.2,109.91-85.2h-81.05l15.29-42.6l88.87,0c56.8,0,83.71-85.2,83.71-85.2
- H539.99c-2.48-4.72-5.03-9.43-5.03-13.01c0-18.5,49.93-30.55,48.36-60.4c-4.08-16.85,2.51-25.92,2.51-25.92
- c-4.2,9.35,0.01,19.79,2.11,24c0.03,0,0.06,0,0.1,0c13.97,0,41.9,15.36,59.18,15.36c11.43,0,20.41-2.37,25.19-3.97
- c0.69-3.96,0.82-14.29-14.36-20.96c0,0,18.37,0.16,18.15,19.52c0,0-2.09,31.42,0.7,54.29c0.55,4.48,1.7,8.11,3.22,11.09h-0.52
- c-13.7,55.08-55.31,85.2-55.31,85.2h60.7l-51.92,142.02l-0.04-0.01H510.67c-56.8,0-85.2,85.2-85.2,85.2H601.9
- c56.8,0,111.69,5.33,144.18-85.2l40.77-113.6C810.25,293.33,776.46,249.98,724.15,245.36z"/>
- <path class="st56" d="M56.25,489.25H18.47c-8.25,0-14.94,6.69-14.94,14.94c0,8.25,6.69,14.94,14.94,14.94h37.78
- c8.25,0,14.94-6.69,14.94-14.94C71.19,495.94,64.5,489.25,56.25,489.25z"/>
- <path class="st56" d="M171.38,399.61h120.5c8.25,0,14.94-6.69,14.94-14.94c0-8.25-6.69-14.94-14.94-14.94H102.14
- c-8.25,0-14.94,6.69-14.94,14.94c0,8.25,6.69,14.94,14.94,14.94H171.38z"/>
- <path class="st56" d="M180.84,339.85h162.85c8.25,0,14.94-6.69,14.94-14.94c0-8.25-6.69-14.94-14.94-14.94H180.84
- c-8.25,0-14.94,6.69-14.94,14.94C165.9,333.17,172.59,339.85,180.84,339.85z"/>
- </g>
-</g>
-</svg>
diff --git a/resources/tools/dash/app/pal/static/img/logo.svg b/resources/tools/dash/app/pal/static/img/logo.svg
deleted file mode 100644
index 689757e3fd..0000000000
--- a/resources/tools/dash/app/pal/static/img/logo.svg
+++ /dev/null
@@ -1,348 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
- viewBox="0 0 1000 568.31" style="enable-background:new 0 0 1000 568.31;" xml:space="preserve">
-<style type="text/css">
- .st0{fill:#1DCAD3;}
- .st1{fill:#36B0C9;}
- .st2{fill:#231F20;}
- .st3{fill:#FFFFFF;}
- .st4{fill:#9164CC;}
- .st5{clip-path:url(#SVGID_2_);fill:url(#SVGID_3_);}
- .st6{fill:#201747;}
- .st7{fill-rule:evenodd;clip-rule:evenodd;fill:#10CFC9;}
- .st8{clip-path:url(#SVGID_5_);fill:#231F20;}
- .st9{fill-rule:evenodd;clip-rule:evenodd;fill:#231F20;}
- .st10{clip-path:url(#SVGID_7_);fill:#FFFFFF;}
- .st11{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
- .st12{fill:#8CCEAF;}
- .st13{fill:#008476;}
- .st14{fill:#25BCBD;}
- .st15{fill:#004D70;}
- .st16{fill:#20BBBB;}
- .st17{fill:#024D70;}
- .st18{fill-rule:evenodd;clip-rule:evenodd;fill:#F58B1F;}
- .st19{fill-rule:evenodd;clip-rule:evenodd;fill:#18335B;}
- .st20{clip-path:url(#SVGID_9_);}
- .st21{clip-path:url(#SVGID_11_);}
- .st22{fill:#18335B;}
- .st23{fill:#F58B1F;}
- .st24{clip-path:url(#SVGID_15_);}
- .st25{clip-path:url(#SVGID_17_);}
- .st26{clip-path:url(#SVGID_21_);}
- .st27{clip-path:url(#SVGID_23_);}
- .st28{clip-path:url(#SVGID_27_);}
- .st29{clip-path:url(#SVGID_29_);}
- .st30{clip-path:url(#SVGID_33_);}
- .st31{clip-path:url(#SVGID_35_);}
- .st32{clip-path:url(#SVGID_39_);}
- .st33{clip-path:url(#SVGID_41_);}
- .st34{fill:#416BA9;}
- .st35{fill:#73C3D5;}
- .st36{opacity:0.8;}
- .st37{fill:#3A3A3A;}
- .st38{fill:url(#SVGID_44_);}
- .st39{fill:none;stroke:#000000;stroke-width:6.3384;}
- .st40{fill:none;stroke:#000000;stroke-width:3.1692;}
- .st41{fill:#48494B;}
- .st42{fill:#C1986C;}
- .st43{fill:url(#SVGID_63_);}
- .st44{fill:url(#SVGID_64_);}
- .st45{fill:url(#SVGID_65_);}
- .st46{fill:url(#SVGID_66_);}
- .st47{fill:url(#SVGID_67_);}
- .st48{fill:#4D4E4E;}
- .st49{fill:#27B373;}
- .st50{fill:#5DC4CD;}
- .st51{fill:#1E8756;}
- .st52{fill:#3D1152;}
- .st53{fill:#922C48;}
- .st54{fill-rule:evenodd;clip-rule:evenodd;fill:#922C48;}
- .st55{fill:#404041;}
- .st56{fill:#EC1C24;}
- .st57{fill:#373A36;}
- .st58{fill:#808184;}
- .st59{fill:#262261;}
- .st60{fill:#6FCBDC;}
- .st61{fill:#2F3436;}
- .st62{fill:#5F97D0;}
- .st63{fill:#132428;}
- .st64{fill:#85C041;}
- .st65{fill:#677784;}
- .st66{fill:url(#SVGID_68_);}
- .st67{opacity:0.2;clip-path:url(#SVGID_70_);}
- .st68{fill:#FFFEFA;}
- .st69{opacity:0.1;}
- .st70{fill:url(#SVGID_71_);}
- .st71{opacity:0.3;}
- .st72{opacity:0.08;}
- .st73{opacity:0.1;fill:url(#Wordmark_1_);}
- .st74{fill:url(#SVGID_104_);}
- .st75{opacity:0.6;fill:url(#SVGID_107_);}
- .st76{opacity:0.4;}
- .st77{fill:url(#SVGID_110_);}
- .st78{opacity:0.6;fill:url(#SVGID_113_);}
- .st79{fill:url(#SVGID_116_);}
- .st80{opacity:0.6;fill:url(#SVGID_119_);}
- .st81{fill:url(#SVGID_122_);}
- .st82{opacity:0.6;fill:url(#SVGID_125_);}
- .st83{fill:url(#SVGID_128_);}
- .st84{opacity:0.6;fill:url(#SVGID_131_);}
- .st85{fill:#221F1F;}
- .st86{fill:none;}
- .st87{fill:#00416B;}
- .st88{opacity:0.8;fill:url(#XMLID_323_);}
- .st89{fill:#4197CB;}
- .st90{fill:#003E52;}
- .st91{fill:#3F96B4;}
- .st92{fill:#B9DBE5;}
- .st93{opacity:0.3;fill:#231F20;}
- .st94{opacity:0.3;fill:#FFFFFF;}
- .st95{fill:#050013;}
- .st96{fill:#E87200;}
- .st97{fill:#FCB813;}
- .st98{fill:#3D3935;}
- .st99{fill:#FFB600;}
- .st100{fill:#FCB814;}
- .st101{fill:#F48120;}
- .st102{fill:#EF4E25;}
- .st103{fill:#ED3024;}
- .st104{fill:#E0592A;}
- .st105{fill:#00ADBB;}
- .st106{fill:#00829B;}
- .st107{fill:#93D500;}
- .st108{fill:#4D5A31;}
- .st109{fill:#6BA43A;}
- .st110{fill:#424143;}
- .st111{fill-rule:evenodd;clip-rule:evenodd;fill:#C7E6B4;}
- .st112{fill-rule:evenodd;clip-rule:evenodd;fill:#5A9891;}
- .st113{fill-rule:evenodd;clip-rule:evenodd;fill:#127870;}
- .st114{fill-rule:evenodd;clip-rule:evenodd;fill:#5CCFD5;}
- .st115{fill-rule:evenodd;clip-rule:evenodd;fill:#ACD5CD;}
- .st116{fill-rule:evenodd;clip-rule:evenodd;fill:#B5ECC9;}
- .st117{fill-rule:evenodd;clip-rule:evenodd;fill:#A1D683;}
- .st118{fill-rule:evenodd;clip-rule:evenodd;fill:#DEF0D3;}
- .st119{fill-rule:evenodd;clip-rule:evenodd;fill:#91B9B4;}
- .st120{fill-rule:evenodd;clip-rule:evenodd;fill:#006860;}
- .st121{fill-rule:evenodd;clip-rule:evenodd;fill:#00ADBB;}
- .st122{fill-rule:evenodd;clip-rule:evenodd;fill:#B4E7E9;}
- .st123{fill-rule:evenodd;clip-rule:evenodd;fill:#007565;}
- .st124{fill-rule:evenodd;clip-rule:evenodd;fill:#00CE7C;}
- .st125{fill-rule:evenodd;clip-rule:evenodd;fill:#5FD896;}
- .st126{fill:#007DA5;}
- .st127{fill:#313032;}
- .st128{fill:#24272A;}
- .st129{fill:#00AFAA;}
- .st130{fill:#66C9BA;}
- .st131{fill:#0069A7;}
- .st132{fill:#002F87;}
- .st133{fill:#8BC53F;}
- .st134{fill:#1A1A1A;}
- .st135{fill:#0095D6;}
- .st136{fill:#003F5F;}
- .st137{fill:#2D317C;}
- .st138{fill:#41BFBF;}
- .st139{fill:#293C97;}
- .st140{fill:#52C2BD;}
- .st141{fill:url(#SVGID_134_);}
- .st142{fill:url(#SVGID_135_);}
- .st143{fill:url(#SVGID_136_);}
- .st144{fill:#0DBEEA;}
- .st145{fill:#097EC2;}
- .st146{fill:#133C63;}
- .st147{fill:#3B91CF;}
- .st148{fill:#C8DEE8;}
- .st149{fill:#629BBA;}
- .st150{fill:#F8BE19;}
- .st151{fill:url(#SVGID_137_);}
- .st152{fill:url(#SVGID_138_);}
- .st153{fill:url(#SVGID_139_);}
- .st154{fill:#00233B;}
- .st155{fill:url(#SVGID_140_);}
- .st156{fill:url(#SVGID_141_);}
- .st157{fill:url(#SVGID_142_);}
- .st158{fill:url(#SVGID_143_);}
- .st159{fill:url(#SVGID_144_);}
- .st160{fill:url(#SVGID_145_);}
- .st161{fill:url(#SVGID_146_);}
- .st162{fill:url(#SVGID_147_);}
- .st163{fill:url(#SVGID_148_);}
- .st164{fill:url(#SVGID_149_);}
- .st165{fill:url(#SVGID_150_);}
- .st166{fill:url(#SVGID_151_);}
- .st167{fill:url(#SVGID_152_);}
- .st168{fill:url(#SVGID_153_);}
- .st169{fill:url(#SVGID_154_);}
- .st170{fill:url(#SVGID_155_);}
- .st171{fill:url(#SVGID_156_);}
- .st172{fill:url(#SVGID_157_);}
- .st173{fill:url(#SVGID_158_);}
- .st174{fill:url(#SVGID_159_);}
- .st175{fill:url(#SVGID_160_);}
- .st176{fill:url(#SVGID_161_);}
- .st177{fill:url(#SVGID_162_);}
- .st178{fill:url(#SVGID_163_);}
- .st179{fill:url(#SVGID_164_);}
- .st180{fill:url(#SVGID_165_);}
- .st181{fill:url(#SVGID_166_);}
- .st182{fill:url(#SVGID_167_);}
- .st183{fill:url(#SVGID_168_);}
- .st184{fill:url(#SVGID_169_);}
- .st185{fill:url(#SVGID_170_);}
- .st186{fill:url(#SVGID_171_);}
- .st187{fill:url(#SVGID_172_);}
- .st188{fill:url(#SVGID_173_);}
- .st189{fill:url(#SVGID_174_);}
- .st190{fill:url(#SVGID_175_);}
- .st191{fill:url(#SVGID_176_);}
- .st192{fill:url(#SVGID_177_);}
- .st193{fill:url(#SVGID_178_);}
- .st194{fill:#C31230;}
- .st195{fill:#807F82;}
- .st196{fill-rule:evenodd;clip-rule:evenodd;fill:#C31230;}
- .st197{fill-rule:evenodd;clip-rule:evenodd;fill:#807F82;}
- .st198{fill:#2D2D2D;}
- .st199{display:none;fill:#2D2D2D;}
- .st200{fill:#D11F3C;}
- .st201{fill:#E42C4C;stroke:#E42C4C;stroke-width:1.0503;stroke-miterlimit:10;}
- .st202{display:none;fill:#231F20;}
- .st203{display:none;fill:#FFFFFF;}
- .st204{fill:#FF7F30;}
- .st205{opacity:0.3;fill:#FF7F30;}
- .st206{opacity:0.6;fill:#FF7F30;}
- .st207{opacity:0.7;fill:#FF7F30;}
- .st208{fill:#221C35;}
- .st209{fill:#1B98D5;}
- .st210{fill:#173963;}
- .st211{fill:#009ADE;}
- .st212{fill:#003764;}
- .st213{fill:#2A7DE1;}
- .st214{opacity:0.4;clip-path:url(#XMLID_324_);fill:#221F1F;}
- .st215{fill:#002A3A;}
- .st216{fill:#0033A1;}
- .st217{fill:url(#SVGID_179_);}
- .st218{fill:url(#SVGID_180_);}
- .st219{fill:url(#SVGID_181_);}
- .st220{fill:url(#SVGID_182_);}
- .st221{fill:#007EC4;}
- .st222{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_183_);}
- .st223{fill-rule:evenodd;clip-rule:evenodd;fill:#E6E7E8;}
- .st224{fill:#009345;}
- .st225{fill:#BBBCB8;}
- .st226{fill:#72C0EB;}
- .st227{fill:#939598;}
- .st228{fill-rule:evenodd;clip-rule:evenodd;fill:#2CB8EB;}
- .st229{fill:#2CB8EB;}
- .st230{fill:#81B83A;}
- .st231{fill-rule:evenodd;clip-rule:evenodd;fill:#81B83A;}
- .st232{enable-background:new ;}
- .st233{fill:#FF6F3E;}
- .st234{fill:#12143D;}
- .st235{fill:url(#SVGID_184_);}
- .st236{fill:url(#SVGID_185_);}
- .st237{fill:url(#SVGID_186_);}
- .st238{fill:url(#SVGID_187_);}
- .st239{fill:url(#SVGID_188_);}
- .st240{fill:url(#SVGID_189_);}
- .st241{fill:url(#SVGID_190_);}
- .st242{fill:url(#SVGID_191_);}
- .st243{fill:url(#SVGID_192_);}
- .st244{fill:#7C51A0;}
- .st245{fill:#9F66A9;}
- .st246{fill:#9F80B9;}
- .st247{fill:url(#SVGID_193_);}
- .st248{fill:url(#SVGID_194_);}
- .st249{fill:url(#SVGID_195_);}
- .st250{fill:url(#SVGID_196_);}
- .st251{fill:#2D3136;}
- .st252{fill:#76777A;}
- .st253{fill:#A7A8A9;}
- .st254{fill:#0082CA;}
- .st255{fill:#FFB259;}
- .st256{fill:#385CAD;}
- .st257{fill:#7BA0C4;}
- .st258{fill:#EBA900;}
- .st259{fill:#929497;}
- .st260{opacity:0.7;fill:#FFFFFF;}
- .st261{fill:#016BAF;}
- .st262{fill:#343432;}
- .st263{fill:#6D6E70;}
- .st264{fill:#F4B01B;}
- .st265{fill:#293271;}
- .st266{fill:#A1D33C;}
- .st267{fill:#212322;}
- .st268{fill:#0047BA;}
- .st269{fill:#969CDE;}
- .st270{fill:#047BC1;}
- .st271{fill:url(#SVGID_197_);}
- .st272{fill:url(#SVGID_198_);}
- .st273{fill:url(#SVGID_199_);}
- .st274{fill:url(#SVGID_200_);}
- .st275{fill:url(#SVGID_201_);}
- .st276{fill:url(#SVGID_202_);}
- .st277{fill:url(#SVGID_203_);}
- .st278{fill:#13517C;}
- .st279{fill:#0077A6;}
- .st280{fill:none;stroke:#231F20;stroke-width:5.9036;stroke-miterlimit:10;}
- .st281{fill:#00A94F;}
- .st282{fill:none;stroke:#231F20;stroke-width:3.2172;stroke-miterlimit:10;}
- .st283{fill:#59595C;}
- .st284{opacity:0.349;fill:#F9AE19;}
- .st285{opacity:0.349;fill:#E99F22;}
- .st286{opacity:0.349;fill:#E47D25;}
- .st287{fill:#F9AE19;}
- .st288{fill:#E99F22;}
- .st289{fill:#F09B20;}
- .st290{fill:#E47D25;}
- .st291{fill:#E89223;}
- .st292{opacity:0.651;fill:#F9AE19;}
- .st293{fill:#E68825;}
- .st294{opacity:0.651;fill:#E99F22;}
- .st295{fill:#EB8D23;}
- .st296{opacity:0.7725;fill:#EF9B21;}
- .st297{opacity:0.651;fill:#E47D25;}
- .st298{opacity:0.7725;fill:#EA9622;}
- .st299{fill:url(#SVGID_204_);}
- .st300{fill:#55575B;}
- .st301{fill:#EE424E;}
- .st302{fill:#34424B;}
-</style>
-<g>
- <g>
- <path class="st55" d="M772.88,526c9.95,0,15.7,5.53,15.7,15.48c0,10.17-5.75,15.48-15.7,15.48c-9.95,0-15.48-5.31-15.48-15.48
- C757.4,531.53,762.93,526,772.88,526z"/>
- <path class="st55" d="M832.94,393.35c8.18,0,13.71,3.32,13.71,12.38c0,9.29-5.53,12.6-13.71,12.6c-8.4,0-14.15-3.32-14.15-12.6
- C818.79,396.67,824.54,393.35,832.94,393.35z M821.22,438.67h22.99V554.3h-22.99V438.67z"/>
- <path class="st55" d="M934.56,435.58c36.25,0,61.9,26.09,61.9,61.24c0,34.71-25.65,60.58-61.9,60.58
- c-35.82,0-61.69-25.87-61.69-60.58C872.88,461.67,898.75,435.58,934.56,435.58z M934.56,536.18c23.66,0,39.79-17.03,39.79-39.36
- c0-22.77-16.14-40.02-39.79-40.02c-23.44,0-39.36,17.25-39.36,40.02C895.21,519.15,911.13,536.18,934.56,536.18z"/>
- </g>
- <g>
- <path class="st56" d="M724.15,245.36c-0.97-8.4-17.24-16.23-17.24-16.23c-6.81-4.71-8.03-16.23-7.16-20.6
- c0.87-4.36,15.01-36.66,15.01-36.66c11.52-11.35,31.6-35.44,33.87-48.88c1.07-6.31-3.14-38.93,5.41-49.75
- c5.63-7.12,22.35-15.36,25.84-18.16c3.49-2.79,6.28-11,4.71-14.84c-1.57-3.84-27.41-4.02-27.41-4.02s-2.16-10.79-17.62-11.8
- c-6.05-0.2-10.18,1.3-10.18,1.3c1.78-0.89,4.69-2.18,6.78-3.1c-0.54-3.56-1.89-11.45-3.24-11.72c-1.43-0.29-4.73,5.28-6.27,7.21
- l0.28,3.42l-1.11-2.85l-1.63-4.2l-0.02-0.06h0c-0.64-1.74-1.36-3.37-2.08-3.53c-1.57-0.34-38.06,50.81-42.6,57.44
- c-4.54,6.63-9.25,17.81-33.87,25.49c-17.37,5.42-53.43,6.32-81.13,15.16l0,0c-14.25,1.43-53.29,33.79-83.15,32.62
- c-20.05-0.78-35.61-6.28-39.57-15.25c0,0,4.19,41.32,25.6,43.64c0,0,13.5,2.33,26.53-9.31c0,0-3.72,7.36-8.38,9.5
- c0,0,18.07-4.62,29.06-12.94c3.37-7.06,9.22-17.4,17.46-25.17c0.53-0.52,0.86-0.8,0.86-0.8c-0.29,0.26-0.57,0.53-0.86,0.8
- c-2.65,2.6-10.68,11.48-11.86,24.73c-0.24,2.64-0.21,5.44,0.2,8.41c0,8.9,9.66,20.78,9.66,26.19c0,7.16-16.76,20.25-17.81,23.74
- c-0.58,1.95-2.35,16.91,0.69,29.77h-59.09c-56.8,0-78.68,56.8-78.68,56.8l-24.4,68h-10.52c-8.25,0-14.94,6.69-14.94,14.94
- c0,8.19,6.59,14.81,14.75,14.92l-0.01,0.02h0.2h5.92c8.25,0,14.94,6.69,14.94,14.94c0,8.25-6.69,14.94-14.94,14.94H197.01
- c-8.25,0-14.94,6.69-14.94,14.94c0,8.25,6.69,14.94,14.94,14.94h154.15c8.25,0,14.94,6.69,14.94,14.94
- c0,8.25-6.69,14.94-14.94,14.94h-32.39H146.48h-32.39c-8.25,0-14.94,6.69-14.94,14.94c0,8.25,6.69,14.94,14.94,14.94h183.08
- l-13.71,38.21h113.6l35.67-99.4h1.72c79.83,0,109.91-85.2,109.91-85.2h-81.05l15.29-42.6l88.87,0c56.8,0,83.71-85.2,83.71-85.2
- H539.99c-2.48-4.72-5.03-9.43-5.03-13.01c0-18.5,49.93-30.55,48.36-60.4c-4.08-16.85,2.51-25.92,2.51-25.92
- c-4.2,9.35,0.01,19.79,2.11,24c0.03,0,0.06,0,0.1,0c13.97,0,41.9,15.36,59.18,15.36c11.43,0,20.41-2.37,25.19-3.97
- c0.69-3.96,0.82-14.29-14.36-20.96c0,0,18.37,0.16,18.15,19.52c0,0-2.09,31.42,0.7,54.29c0.55,4.48,1.7,8.11,3.22,11.09h-0.52
- c-13.7,55.08-55.31,85.2-55.31,85.2h60.7l-51.92,142.02l-0.04-0.01H510.67c-56.8,0-85.2,85.2-85.2,85.2H601.9
- c56.8,0,111.69,5.33,144.18-85.2l40.77-113.6C810.25,293.33,776.46,249.98,724.15,245.36z"/>
- <path class="st56" d="M56.25,489.25H18.47c-8.25,0-14.94,6.69-14.94,14.94c0,8.25,6.69,14.94,14.94,14.94h37.78
- c8.25,0,14.94-6.69,14.94-14.94C71.19,495.94,64.5,489.25,56.25,489.25z"/>
- <path class="st56" d="M171.38,399.61h120.5c8.25,0,14.94-6.69,14.94-14.94c0-8.25-6.69-14.94-14.94-14.94H102.14
- c-8.25,0-14.94,6.69-14.94,14.94c0,8.25,6.69,14.94,14.94,14.94H171.38z"/>
- <path class="st56" d="M180.84,339.85h162.85c8.25,0,14.94-6.69,14.94-14.94c0-8.25-6.69-14.94-14.94-14.94H180.84
- c-8.25,0-14.94,6.69-14.94,14.94C165.9,333.17,172.59,339.85,180.84,339.85z"/>
- </g>
-</g>
-</svg>
diff --git a/resources/tools/dash/app/pal/stats/__init__.py b/resources/tools/dash/app/pal/stats/__init__.py
deleted file mode 100644
index 5692432123..0000000000
--- a/resources/tools/dash/app/pal/stats/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
diff --git a/resources/tools/dash/app/pal/stats/graphs.py b/resources/tools/dash/app/pal/stats/graphs.py
deleted file mode 100644
index 42f23da5aa..0000000000
--- a/resources/tools/dash/app/pal/stats/graphs.py
+++ /dev/null
@@ -1,138 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""
-"""
-
-import plotly.graph_objects as go
-import pandas as pd
-
-from datetime import datetime, timedelta
-
-def select_data(data: pd.DataFrame, itm:str, start: datetime,
- end: datetime) -> pd.DataFrame:
- """Select the data for graphs from the provided data frame.
-
- :param data: Data frame with data for graphs.
- :param itm: Item (in this case job name) which data will be selected from
- the input data frame.
- :param start: The date (and time) when the selected data starts.
- :param end: The date (and time) when the selected data ends.
- :type data: pandas.DataFrame
- :type itm: str
- :type start: datetime.datetime
- :type end: datetime.datetime
- :returns: A data frame with selected data.
- :rtype: pandas.DataFrame
- """
-
- df = data.loc[
- (data["job"] == itm) &
- (data["start_time"] >= start) & (data["start_time"] <= end)
- ].sort_values(by="start_time", ignore_index=True)
- df = df.dropna(subset=["duration", ])
-
- return df
-
-
-def graph_statistics(df: pd.DataFrame, job:str, layout: dict,
- start: datetime=datetime.utcnow()-timedelta(days=180),
- end: datetime=datetime.utcnow()) -> tuple:
- """Generate graphs:
- 1. Passed / failed tests,
- 2. Job durations
- with additional information shown in hover.
-
- :param df: Data frame with input data.
- :param job: The name of job which data will be presented in the graphs.
- :param layout: Layout of plot.ly graph.
- :param start: The date (and time) when the selected data starts.
- :param end: The date (and time) when the selected data ends.
- :type df: pandas.DataFrame
- :type job: str
- :type layout: dict
- :type start: datetime.datetime
- :type end: datetime.datetime
- :returns: Tuple with two generated graphs (pased/failed tests and job
- duration).
- :rtype: tuple(plotly.graph_objects.Figure, plotly.graph_objects.Figure)
- """
-
- data = select_data(df, job, start, end)
- if data.empty:
- return None, None
-
- hover = list()
- for _, row in data.iterrows():
- d_type = "trex" if row["dut_type"] == "none" else row["dut_type"]
- hover_itm = (
- f"date: {row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>"
- f"duration: "
- f"{(int(row['duration']) // 3600):02d}:"
- f"{((int(row['duration']) % 3600) // 60):02d}<br>"
- f"passed: {row['passed']}<br>"
- f"failed: {row['failed']}<br>"
- f"{d_type}-ref: {row['dut_version']}<br>"
- f"csit-ref: {row['job']}/{row['build']}<br>"
- f"hosts: {', '.join(row['hosts'])}"
- )
- hover.append(hover_itm)
-
- # Job durations:
- fig_duration = go.Figure(
- data=go.Scatter(
- x=data["start_time"],
- y=data["duration"],
- name=u"Duration",
- text=hover,
- hoverinfo=u"text"
- )
- )
-
- tickvals = [0, ]
- step = max(data["duration"]) / 5
- for i in range(5):
- tickvals.append(int(step * (i + 1)))
- layout_duration = layout.get("plot-stats-duration", dict())
- if layout_duration:
- layout_duration["yaxis"]["tickvals"] = tickvals
- layout_duration["yaxis"]["ticktext"] = [
- f"{(val // 3600):02d}:{((val % 3600) // 60):02d}" \
- for val in tickvals
- ]
- fig_duration.update_layout(layout_duration)
-
- # Passed / failed:
- fig_passed = go.Figure(
- data=[
- go.Bar(
- x=data["start_time"],
- y=data["passed"],
- name=u"Passed",
- hovertext=hover,
- hoverinfo=u"text"
- ),
- go.Bar(
- x=data["start_time"],
- y=data["failed"],
- name=u"Failed",
- hovertext=hover,
- hoverinfo=u"text"
- )
- ]
- )
- layout_pf = layout.get("plot-stats-passed", dict())
- if layout_pf:
- fig_passed.update_layout(layout_pf)
-
- return fig_passed, fig_duration
diff --git a/resources/tools/dash/app/pal/stats/layout.py b/resources/tools/dash/app/pal/stats/layout.py
deleted file mode 100644
index 1d271cb265..0000000000
--- a/resources/tools/dash/app/pal/stats/layout.py
+++ /dev/null
@@ -1,920 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Plotly Dash HTML layout override.
-"""
-
-import logging
-import pandas as pd
-import dash_bootstrap_components as dbc
-
-from flask import Flask
-from dash import dcc
-from dash import html
-from dash import callback_context, no_update
-from dash import Input, Output, State
-from dash.exceptions import PreventUpdate
-from yaml import load, FullLoader, YAMLError
-from datetime import datetime, timedelta
-from copy import deepcopy
-
-from ..utils.constants import Constants as C
-from ..utils.utils import show_tooltip, gen_new_url, get_date, get_ttypes, \
- get_cadences, get_test_beds, get_job, generate_options, set_job_params
-from ..utils.url_processing import url_decode
-from ..data.data import Data
-from .graphs import graph_statistics, select_data
-
-
-class Layout:
- """The layout of the dash app and the callbacks.
- """
-
- def __init__(self, app: Flask, html_layout_file: str,
- graph_layout_file: str, data_spec_file: str, tooltip_file: str,
- time_period: int=None) -> None:
- """Initialization:
- - save the input parameters,
- - read and pre-process the data,
- - prepare data for the control panel,
- - read HTML layout file,
- - read tooltips from the tooltip file.
-
- :param app: Flask application running the dash application.
- :param html_layout_file: Path and name of the file specifying the HTML
- layout of the dash application.
- :param graph_layout_file: Path and name of the file with layout of
- plot.ly graphs.
- :param data_spec_file: Path and name of the file specifying the data to
- be read from parquets for this application.
- :param tooltip_file: Path and name of the yaml file specifying the
- tooltips.
- :param time_period: It defines the time period for data read from the
- parquets in days from now back to the past.
- :type app: Flask
- :type html_layout_file: str
- :type graph_layout_file: str
- :type data_spec_file: str
- :type tooltip_file: str
- :type time_period: int
- """
-
- # Inputs
- self._app = app
- self._html_layout_file = html_layout_file
- self._graph_layout_file = graph_layout_file
- self._data_spec_file = data_spec_file
- self._tooltip_file = tooltip_file
- self._time_period = time_period
-
- # Read the data:
- data_stats, data_mrr, data_ndrpdr = Data(
- data_spec_file=self._data_spec_file,
- debug=True
- ).read_stats(days=self._time_period)
-
- df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
-
- # Pre-process the data:
- data_stats = data_stats[~data_stats.job.str.contains("-verify-")]
- data_stats = data_stats[~data_stats.job.str.contains("-coverage-")]
- data_stats = data_stats[~data_stats.job.str.contains("-iterative-")]
- data_stats = data_stats[["job", "build", "start_time", "duration"]]
-
- data_time_period = \
- (datetime.utcnow() - data_stats["start_time"].min()).days
- if self._time_period > data_time_period:
- self._time_period = data_time_period
-
- jobs = sorted(list(data_stats["job"].unique()))
- d_job_info = {
- "job": list(),
- "dut": list(),
- "ttype": list(),
- "cadence": list(),
- "tbed": list()
- }
- for job in jobs:
- lst_job = job.split("-")
- d_job_info["job"].append(job)
- d_job_info["dut"].append(lst_job[1])
- d_job_info["ttype"].append(lst_job[3])
- d_job_info["cadence"].append(lst_job[4])
- d_job_info["tbed"].append("-".join(lst_job[-2:]))
- self.job_info = pd.DataFrame.from_dict(d_job_info)
-
- self._default = set_job_params(self.job_info, C.STATS_DEFAULT_JOB)
-
- tst_info = {
- "job": list(),
- "build": list(),
- "dut_type": list(),
- "dut_version": list(),
- "hosts": list(),
- "passed": list(),
- "failed": list(),
- "lst_failed": list()
- }
- for job in jobs:
- df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
- builds = df_job["build"].unique()
- for build in builds:
- df_build = df_job.loc[(df_job["build"] == build)]
- tst_info["job"].append(job)
- tst_info["build"].append(build)
- tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
- tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
- tst_info["hosts"].append(df_build["hosts"].iloc[-1])
- try:
- passed = df_build.value_counts(subset="passed")[True]
- except KeyError:
- passed = 0
- try:
- failed = df_build.value_counts(subset="passed")[False]
- failed_tests = df_build.loc[(df_build["passed"] == False)]\
- ["test_id"].to_list()
- l_failed = list()
- for tst in failed_tests:
- lst_tst = tst.split(".")
- suite = lst_tst[-2].replace("2n1l-", "").\
- replace("1n1l-", "").replace("2n-", "")
- l_failed.append(f"{suite.split('-')[0]}-{lst_tst[-1]}")
- except KeyError:
- failed = 0
- l_failed = list()
- tst_info["passed"].append(passed)
- tst_info["failed"].append(failed)
- tst_info["lst_failed"].append(sorted(l_failed))
-
- self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
-
- # Read from files:
- self._html_layout = ""
- self._graph_layout = None
- self._tooltips = dict()
-
- try:
- with open(self._html_layout_file, "r") as file_read:
- self._html_layout = file_read.read()
- except IOError as err:
- raise RuntimeError(
- f"Not possible to open the file {self._html_layout_file}\n{err}"
- )
-
- try:
- with open(self._graph_layout_file, "r") as file_read:
- self._graph_layout = load(file_read, Loader=FullLoader)
- except IOError as err:
- raise RuntimeError(
- f"Not possible to open the file {self._graph_layout_file}\n"
- f"{err}"
- )
- except YAMLError as err:
- raise RuntimeError(
- f"An error occurred while parsing the specification file "
- f"{self._graph_layout_file}\n{err}"
- )
-
- try:
- with open(self._tooltip_file, "r") as file_read:
- self._tooltips = load(file_read, Loader=FullLoader)
- except IOError as err:
- logging.warning(
- f"Not possible to open the file {self._tooltip_file}\n{err}"
- )
- except YAMLError as err:
- logging.warning(
- f"An error occurred while parsing the specification file "
- f"{self._tooltip_file}\n{err}"
- )
-
-
- self._default_fig_passed, self._default_fig_duration = graph_statistics(
- self.data, self._default["job"], self.layout
- )
-
- # Callbacks:
- if self._app is not None and hasattr(self, 'callbacks'):
- self.callbacks(self._app)
-
- @property
- def html_layout(self) -> dict:
- return self._html_layout
-
- @property
- def data(self) -> pd.DataFrame:
- return self._data
-
- @property
- def layout(self) -> dict:
- return self._graph_layout
-
- @property
- def time_period(self) -> int:
- return self._time_period
-
- @property
- def default(self) -> any:
- return self._default
-
- def add_content(self):
- """Top level method which generated the web page.
-
- It generates:
- - Store for user input data,
- - Navigation bar,
- - Main area with control panel and ploting area.
-
- If no HTML layout is provided, an error message is displayed instead.
-
- :returns: The HTML div with the whole page.
- :rtype: html.Div
- """
-
- if self.html_layout:
- return html.Div(
- id="div-main",
- children=[
- dcc.Store(id="control-panel"),
- dcc.Location(id="url", refresh=False),
- dbc.Row(
- id="row-navbar",
- class_name="g-0",
- children=[
- self._add_navbar(),
- ]
- ),
- dcc.Loading(
- dbc.Offcanvas(
- class_name="w-50",
- id="offcanvas-metadata",
- title="Detailed Information",
- placement="end",
- is_open=False,
- children=[
- dbc.Row(id="row-metadata")
- ]
- )
- ),
- dbc.Row(
- id="row-main",
- class_name="g-0",
- children=[
- self._add_ctrl_col(),
- self._add_plotting_col(),
- ]
- )
- ]
- )
- else:
- return html.Div(
- id="div-main-error",
- children=[
- dbc.Alert(
- [
- "An Error Occured",
- ],
- color="danger",
- ),
- ]
- )
-
- def _add_navbar(self):
- """Add nav element with navigation panel. It is placed on the top.
-
- :returns: Navigation bar.
- :rtype: dbc.NavbarSimple
- """
- return dbc.NavbarSimple(
- id="navbarsimple-main",
- children=[
- dbc.NavItem(
- dbc.NavLink(
- "Continuous Performance Statistics",
- disabled=True,
- external_link=True,
- href="#"
- )
- )
- ],
- brand="Dashboard",
- brand_href="/",
- brand_external_link=True,
- class_name="p-2",
- fluid=True,
- )
-
- def _add_ctrl_col(self) -> dbc.Col:
- """Add column with controls. It is placed on the left side.
-
- :returns: Column with the control panel.
- :rtype: dbc.Col
- """
- return dbc.Col(
- id="col-controls",
- children=[
- self._add_ctrl_panel(),
- ],
- )
-
- def _add_plotting_col(self) -> dbc.Col:
- """Add column with plots and tables. It is placed on the right side.
-
- :returns: Column with tables.
- :rtype: dbc.Col
- """
- return dbc.Col(
- id="col-plotting-area",
- children=[
- dbc.Row( # Passed / failed tests
- id="row-graph-passed",
- class_name="g-0 p-2",
- children=[
- dcc.Loading(children=[
- dcc.Graph(
- id="graph-passed",
- figure=self._default_fig_passed
- )
- ])
- ]
- ),
- dbc.Row( # Duration
- id="row-graph-duration",
- class_name="g-0 p-2",
- children=[
- dcc.Loading(children=[
- dcc.Graph(
- id="graph-duration",
- figure=self._default_fig_duration
- )
- ])
- ]
- ),
- dbc.Row(
- class_name="g-0 p-2",
- align="center",
- justify="start",
- children=[
- dbc.Col( # Download
- width=2,
- children=[
- dcc.Loading(children=[
- dbc.Button(
- id="btn-download-data",
- children=show_tooltip(self._tooltips,
- "help-download", "Download Data"),
- class_name="me-1",
- color="info"
- ),
- dcc.Download(id="download-data")
- ])
- ]
- ),
- dbc.Col( # Show URL
- width=10,
- children=[
- dbc.InputGroup(
- class_name="me-1",
- children=[
- dbc.InputGroupText(
- style=C.URL_STYLE,
- children=show_tooltip(
- self._tooltips,
- "help-url", "URL",
- "input-url"
- )
- ),
- dbc.Input(
- id="input-url",
- readonly=True,
- type="url",
- style=C.URL_STYLE,
- value=""
- )
- ]
- )
- ]
- )
- ]
- )
- ],
- width=9,
- )
-
- def _add_ctrl_panel(self) -> dbc.Row:
- """Add control panel.
-
- :returns: Control panel.
- :rtype: dbc.Row
- """
- return dbc.Row(
- id="row-ctrl-panel",
- class_name="g-0",
- children=[
- dbc.Row(
- class_name="g-0 p-2",
- children=[
- dbc.Row(
- class_name="gy-1",
- children=[
- dbc.Label(
- class_name="p-0",
- children=show_tooltip(self._tooltips,
- "help-dut", "Device under Test")
- ),
- dbc.Row(
- dbc.RadioItems(
- id="ri-duts",
- inline=True,
- value=self.default["dut"],
- options=self.default["duts"]
- )
- )
- ]
- ),
- dbc.Row(
- class_name="gy-1",
- children=[
- dbc.Label(
- class_name="p-0",
- children=show_tooltip(self._tooltips,
- "help-ttype", "Test Type"),
- ),
- dbc.RadioItems(
- id="ri-ttypes",
- inline=True,
- value=self.default["ttype"],
- options=self.default["ttypes"]
- )
- ]
- ),
- dbc.Row(
- class_name="gy-1",
- children=[
- dbc.Label(
- class_name="p-0",
- children=show_tooltip(self._tooltips,
- "help-cadence", "Cadence"),
- ),
- dbc.RadioItems(
- id="ri-cadences",
- inline=True,
- value=self.default["cadence"],
- options=self.default["cadences"]
- )
- ]
- ),
- dbc.Row(
- class_name="gy-1",
- children=[
- dbc.Label(
- class_name="p-0",
- children=show_tooltip(self._tooltips,
- "help-tbed", "Test Bed"),
- ),
- dbc.Select(
- id="dd-tbeds",
- placeholder="Select a test bed...",
- value=self.default["tbed"],
- options=self.default["tbeds"]
- )
- ]
- ),
- dbc.Row(
- class_name="gy-1",
- children=[
- dbc.Alert(
- id="al-job",
- color="info",
- children=self.default["job"]
- )
- ]
- ),
- dbc.Row(
- class_name="g-0 p-2",
- children=[
- dbc.Label(
- class_name="gy-1",
- children=show_tooltip(self._tooltips,
- "help-time-period", "Time Period"),
- ),
- dcc.DatePickerRange(
- id="dpr-period",
- className="d-flex justify-content-center",
- min_date_allowed=\
- datetime.utcnow() - timedelta(
- days=self.time_period),
- max_date_allowed=datetime.utcnow(),
- initial_visible_month=datetime.utcnow(),
- start_date=\
- datetime.utcnow() - timedelta(
- days=self.time_period),
- end_date=datetime.utcnow(),
- display_format="D MMM YY"
- )
- ]
- )
- ]
- ),
- ]
- )
-
- class ControlPanel:
- """A class representing the control panel.
- """
-
- def __init__(self, panel: dict, default: dict) -> None:
- """Initialisation of the control pannel by default values. If
- particular values are provided (parameter "panel") they are set
- afterwards.
-
- :param panel: Custom values to be set to the control panel.
- :param default: Default values to be set to the control panel.
- :type panel: dict
- :type defaults: dict
- """
-
- self._defaults = {
- "ri-ttypes-options": default["ttypes"],
- "ri-cadences-options": default["cadences"],
- "dd-tbeds-options": default["tbeds"],
- "ri-duts-value": default["dut"],
- "ri-ttypes-value": default["ttype"],
- "ri-cadences-value": default["cadence"],
- "dd-tbeds-value": default["tbed"],
- "al-job-children": default["job"]
- }
- self._panel = deepcopy(self._defaults)
- if panel:
- for key in self._defaults:
- self._panel[key] = panel[key]
-
- def set(self, kwargs: dict) -> None:
- """Set the values of the Control panel.
-
- :param kwargs: key - value pairs to be set.
- :type kwargs: dict
- :raises KeyError: If the key in kwargs is not present in the Control
- panel.
- """
- for key, val in kwargs.items():
- if key in self._panel:
- self._panel[key] = val
- else:
- raise KeyError(f"The key {key} is not defined.")
-
- @property
- def defaults(self) -> dict:
- return self._defaults
-
- @property
- def panel(self) -> dict:
- return self._panel
-
- def get(self, key: str) -> any:
- """Returns the value of a key from the Control panel.
-
- :param key: The key which value should be returned.
- :type key: str
- :returns: The value of the key.
- :rtype: any
- :raises KeyError: If the key in kwargs is not present in the Control
- panel.
- """
- return self._panel[key]
-
- def values(self) -> list:
- """Returns the values from the Control panel as a list.
-
- :returns: The values from the Control panel.
- :rtype: list
- """
- return list(self._panel.values())
-
-
- def callbacks(self, app):
- """Callbacks for the whole application.
-
- :param app: The application.
- :type app: Flask
- """
-
- @app.callback(
- Output("control-panel", "data"), # Store
- Output("graph-passed", "figure"),
- Output("graph-duration", "figure"),
- Output("input-url", "value"),
- Output("ri-ttypes", "options"),
- Output("ri-cadences", "options"),
- Output("dd-tbeds", "options"),
- Output("ri-duts", "value"),
- Output("ri-ttypes", "value"),
- Output("ri-cadences", "value"),
- Output("dd-tbeds", "value"),
- Output("al-job", "children"),
- State("control-panel", "data"), # Store
- Input("ri-duts", "value"),
- Input("ri-ttypes", "value"),
- Input("ri-cadences", "value"),
- Input("dd-tbeds", "value"),
- Input("dpr-period", "start_date"),
- Input("dpr-period", "end_date"),
- Input("url", "href")
- )
- def _update_ctrl_panel(cp_data: dict, dut: str, ttype: str, cadence:str,
- tbed: str, start: str, end: str, href: str) -> tuple:
- """Update the application when the event is detected.
-
- :param cp_data: Current status of the control panel stored in
- browser.
- :param dut: Input - DUT name.
- :param ttype: Input - Test type.
- :param cadence: Input - The cadence of the job.
- :param tbed: Input - The test bed.
- :param start: Date and time where the data processing starts.
- :param end: Date and time where the data processing ends.
- :param href: Input - The URL provided by the browser.
- :type cp_data: dict
- :type dut: str
- :type ttype: str
- :type cadence: str
- :type tbed: str
- :type start: str
- :type end: str
- :type href: str
- :returns: New values for web page elements.
- :rtype: tuple
- """
-
- ctrl_panel = self.ControlPanel(cp_data, self.default)
-
- start = get_date(start)
- end = get_date(end)
-
- # Parse the url:
- parsed_url = url_decode(href)
- if parsed_url:
- url_params = parsed_url["params"]
- else:
- url_params = None
-
- trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
- if trigger_id == "ri-duts":
- ttype_opts = generate_options(get_ttypes(self.job_info, dut))
- ttype_val = ttype_opts[0]["value"]
- cad_opts = generate_options(get_cadences(
- self.job_info, dut, ttype_val))
- cad_val = cad_opts[0]["value"]
- tbed_opts = generate_options(get_test_beds(
- self.job_info, dut, ttype_val, cad_val))
- tbed_val = tbed_opts[0]["value"]
- ctrl_panel.set({
- "ri-duts-value": dut,
- "ri-ttypes-options": ttype_opts,
- "ri-ttypes-value": ttype_val,
- "ri-cadences-options": cad_opts,
- "ri-cadences-value": cad_val,
- "dd-tbeds-options": tbed_opts,
- "dd-tbeds-value": tbed_val
- })
- elif trigger_id == "ri-ttypes":
- cad_opts = generate_options(get_cadences(
- self.job_info, ctrl_panel.get("ri-duts-value"), ttype))
- cad_val = cad_opts[0]["value"]
- tbed_opts = generate_options(get_test_beds(
- self.job_info, ctrl_panel.get("ri-duts-value"), ttype,
- cad_val))
- tbed_val = tbed_opts[0]["value"]
- ctrl_panel.set({
- "ri-ttypes-value": ttype,
- "ri-cadences-options": cad_opts,
- "ri-cadences-value": cad_val,
- "dd-tbeds-options": tbed_opts,
- "dd-tbeds-value": tbed_val
- })
- elif trigger_id == "ri-cadences":
- tbed_opts = generate_options(get_test_beds(
- self.job_info, ctrl_panel.get("ri-duts-value"),
- ctrl_panel.get("ri-ttypes-value"), cadence))
- tbed_val = tbed_opts[0]["value"]
- ctrl_panel.set({
- "ri-cadences-value": cadence,
- "dd-tbeds-options": tbed_opts,
- "dd-tbeds-value": tbed_val
- })
- elif trigger_id == "dd-tbeds":
- ctrl_panel.set({
- "dd-tbeds-value": tbed
- })
- elif trigger_id == "dpr-period":
- pass
- elif trigger_id == "url":
- # TODO: Add verification
- if url_params:
- new_job = url_params.get("job", list())[0]
- new_start = url_params.get("start", list())[0]
- new_end = url_params.get("end", list())[0]
- if new_job and new_start and new_end:
- start = get_date(new_start)
- end = get_date(new_end)
- job_params = set_job_params(self.job_info, new_job)
- ctrl_panel = self.ControlPanel(None, job_params)
- else:
- ctrl_panel = self.ControlPanel(cp_data, self.default)
-
- job = get_job(
- self.job_info,
- ctrl_panel.get("ri-duts-value"),
- ctrl_panel.get("ri-ttypes-value"),
- ctrl_panel.get("ri-cadences-value"),
- ctrl_panel.get("dd-tbeds-value")
- )
-
- ctrl_panel.set({"al-job-children": job})
- fig_passed, fig_duration = graph_statistics(self.data, job,
- self.layout, start, end)
-
- ret_val = [
- ctrl_panel.panel,
- fig_passed,
- fig_duration,
- gen_new_url(
- parsed_url,
- {
- "job": job,
- "start": start,
- "end": end
- }
- )
- ]
- ret_val.extend(ctrl_panel.values())
- return ret_val
-
- @app.callback(
- Output("download-data", "data"),
- State("control-panel", "data"), # Store
- State("dpr-period", "start_date"),
- State("dpr-period", "end_date"),
- Input("btn-download-data", "n_clicks"),
- prevent_initial_call=True
- )
- def _download_data(cp_data: dict, start: str, end: str, n_clicks: int):
- """Download the data
-
- :param cp_data: Current status of the control panel stored in
- browser.
- :param start: Date and time where the data processing starts.
- :param end: Date and time where the data processing ends.
- :param n_clicks: Number of clicks on the button "Download".
- :type cp_data: dict
- :type start: str
- :type end: str
- :type n_clicks: int
- :returns: dict of data frame content (base64 encoded) and meta data
- used by the Download component.
- :rtype: dict
- """
- if not (n_clicks):
- raise PreventUpdate
-
- ctrl_panel = self.ControlPanel(cp_data, self.default)
-
- job = get_job(
- self.job_info,
- ctrl_panel.get("ri-duts-value"),
- ctrl_panel.get("ri-ttypes-value"),
- ctrl_panel.get("ri-cadences-value"),
- ctrl_panel.get("dd-tbeds-value")
- )
-
- data = select_data(self.data, job, get_date(start), get_date(end))
- data = data.drop(columns=["job", ])
-
- return dcc.send_data_frame(
- data.T.to_csv, f"{job}-{C.STATS_DOWNLOAD_FILE_NAME}")
-
- @app.callback(
- Output("row-metadata", "children"),
- Output("offcanvas-metadata", "is_open"),
- Input("graph-passed", "clickData"),
- Input("graph-duration", "clickData"),
- prevent_initial_call=True
- )
- def _show_metadata_from_graphs(
- passed_data: dict, duration_data: dict) -> tuple:
- """Generates the data for the offcanvas displayed when a particular
- point in a graph is clicked on.
-
- :param passed_data: The data from the clicked point in the graph
- displaying the pass/fail data.
- :param duration_data: The data from the clicked point in the graph
- displaying the duration data.
- :type passed_data: dict
- :type duration data: dict
- :returns: The data to be displayed on the offcanvas (job statistics
- and the list of failed tests) and the information to show the
- offcanvas.
- :rtype: tuple(list, bool)
- """
-
- if not (passed_data or duration_data):
- raise PreventUpdate
-
- metadata = no_update
- open_canvas = False
- title = "Job Statistics"
- trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
- if trigger_id == "graph-passed":
- graph_data = passed_data["points"][0].get("hovertext", "")
- elif trigger_id == "graph-duration":
- graph_data = duration_data["points"][0].get("text", "")
- if graph_data:
- lst_graph_data = graph_data.split("<br>")
-
- # Prepare list of failed tests:
- job = str()
- build = str()
- for itm in lst_graph_data:
- if "csit-ref:" in itm:
- job, build = itm.split(" ")[-1].split("/")
- break
- if job and build:
- fail_tests = self.data.loc[
- (self.data["job"] == job) &
- (self.data["build"] == build)
- ]["lst_failed"].values[0]
- if not fail_tests:
- fail_tests = None
- else:
- fail_tests = None
-
- # Create the content of the offcanvas:
- metadata = [
- dbc.Card(
- class_name="gy-2 p-0",
- children=[
- dbc.CardHeader(children=[
- dcc.Clipboard(
- target_id="metadata",
- title="Copy",
- style={"display": "inline-block"}
- ),
- title
- ]),
- dbc.CardBody(
- id="metadata",
- class_name="p-0",
- children=[dbc.ListGroup(
- children=[
- dbc.ListGroupItem(
- [
- dbc.Badge(
- x.split(":")[0]
- ),
- x.split(": ")[1]
- ]
- ) for x in lst_graph_data
- ],
- flush=True),
- ]
- )
- ]
- )
- ]
-
- if fail_tests is not None:
- metadata.append(
- dbc.Card(
- class_name="gy-2 p-0",
- children=[
- dbc.CardHeader(
- f"List of Failed Tests ({len(fail_tests)})"
- ),
- dbc.CardBody(
- id="failed-tests",
- class_name="p-0",
- children=[dbc.ListGroup(
- children=[
- dbc.ListGroupItem(x) \
- for x in fail_tests
- ],
- flush=True),
- ]
- )
- ]
- )
- )
-
- open_canvas = True
-
- return metadata, open_canvas
diff --git a/resources/tools/dash/app/pal/stats/layout.yaml b/resources/tools/dash/app/pal/stats/layout.yaml
deleted file mode 100644
index 0a102e4d0a..0000000000
--- a/resources/tools/dash/app/pal/stats/layout.yaml
+++ /dev/null
@@ -1,117 +0,0 @@
-plot-stats-passed:
- autosize: True
- showlegend: False
- yaxis:
- showticklabels: True
- title: "Number of Passed / Failed Tests"
- gridcolor: "rgb(238, 238, 238)"
- linecolor: "rgb(238, 238, 238)"
- showline: True
- zeroline: False
- tickcolor: "rgb(238, 238, 238)"
- linewidth: 1
- showgrid: True
- rangemode: "tozero"
- xaxis:
- title: 'Date [MMDD]'
- type: "date"
- autorange: True
- fixedrange: False
- showgrid: True
- gridcolor: "rgb(238, 238, 238)"
- showline: True
- linecolor: "rgb(238, 238, 238)"
- zeroline: False
- linewidth: 1
- showticklabels: True
- tickcolor: "rgb(238, 238, 238)"
- tickmode: "auto"
- tickformat: "%m%d"
- rangeselector:
- buttons:
- - count: 14
- label: "2w"
- step: "day"
- stepmode: "backward"
- - count: 1
- label: "1m"
- step: "month"
- stepmode: "backward"
- - count: 2
- label: "2m"
- step: "month"
- stepmode: "backward"
- - count: 3
- label: "3m"
- step: "month"
- stepmode: "backward"
- - step: "all"
- margin:
- r: 20
- b: 5
- t: 5
- l: 70
- paper_bgcolor: "#fff"
- plot_bgcolor: "#fff"
- barmode: "stack"
- hoverlabel:
- namelength: -1
-
-plot-stats-duration:
- autosize: True
- showlegend: False
- yaxis:
- showticklabels: True
- title: "Duration [hh:mm]"
- gridcolor: "rgb(238, 238, 238)"
- linecolor: "rgb(238, 238, 238)"
- showline: True
- zeroline: False
- tickmode: "array"
- tickcolor: "rgb(238, 238, 238)"
- linewidth: 1
- showgrid: True
- rangemode: "tozero"
- xaxis:
- title: 'Date [MMDD]'
- type: "date"
- autorange: True
- fixedrange: False
- showgrid: True
- gridcolor: "rgb(238, 238, 238)"
- showline: True
- linecolor: "rgb(238, 238, 238)"
- zeroline: False
- linewidth: 1
- showticklabels: True
- tickcolor: "rgb(238, 238, 238)"
- tickmode: "auto"
- tickformat: "%m%d"
- rangeselector:
- buttons:
- - count: 14
- label: "2w"
- step: "day"
- stepmode: "backward"
- - count: 1
- label: "1m"
- step: "month"
- stepmode: "backward"
- - count: 2
- label: "2m"
- step: "month"
- stepmode: "backward"
- - count: 3
- label: "3m"
- step: "month"
- stepmode: "backward"
- - step: "all"
- margin:
- r: 20
- b: 5
- t: 5
- l: 70
- paper_bgcolor: "#fff"
- plot_bgcolor: "#fff"
- hoverlabel:
- namelength: -1
diff --git a/resources/tools/dash/app/pal/stats/stats.py b/resources/tools/dash/app/pal/stats/stats.py
deleted file mode 100644
index 5b31faca44..0000000000
--- a/resources/tools/dash/app/pal/stats/stats.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Instantiate the Statistics Dash application.
-"""
-import dash
-
-from ..utils.constants import Constants as C
-from .layout import Layout
-
-
-def init_stats(server, time_period=None):
- """Create a Plotly Dash dashboard.
-
- :param server: Flask server.
- :type server: Flask
- :returns: Dash app server.
- :rtype: Dash
- """
-
- dash_app = dash.Dash(
- server=server,
- routes_pathname_prefix=C.STATS_ROUTES_PATHNAME_PREFIX,
- external_stylesheets=C.EXTERNAL_STYLESHEETS
- )
-
- layout = Layout(
- app=dash_app,
- html_layout_file=C.STATS_HTML_LAYOUT_FILE,
- graph_layout_file=C.STATS_GRAPH_LAYOUT_FILE,
- data_spec_file=C.DATA_SPEC_FILE,
- tooltip_file=C.TOOLTIP_FILE,
- time_period=time_period
- )
- dash_app.index_string = layout.html_layout
- dash_app.layout = layout.add_content()
-
- return dash_app.server
diff --git a/resources/tools/dash/app/pal/templates/base_layout.jinja2 b/resources/tools/dash/app/pal/templates/base_layout.jinja2
deleted file mode 100644
index 09b035ee9e..0000000000
--- a/resources/tools/dash/app/pal/templates/base_layout.jinja2
+++ /dev/null
@@ -1,22 +0,0 @@
-<!DOCTYPE html>
-<html lang="en" class="h-100">
-<head>
- <meta charset="utf-8" />
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
- <title>{{ title }}</title>
- <meta property="og:site_name" content="{{ title }}"/>
- <meta property="og:type" content="website"/>
- <meta property="og:title" content="{{ title }}"/>
- <meta property="og:description" content="{{ description }}"/>
- <meta property="og:url" content="https://csit.fd.io/"/>
- <meta name="HandheldFriendly" content="True" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
- <!-- Bootstrap core CSS -->
- <link rel="stylesheet" href="{{ url_for('static', filename='dist/css/bootstrap.min.css') }}" crossorigin="anonymous" />
- <!-- Favicons -->
- <link rel="shortcut icon" href="{{ url_for('static', filename='dist/img/favicon.svg') }}" type="image/x-icon" />
-</head>
-<body class="{{template}}">
- {% block content %}{% endblock %}
-</body>
-</html>
diff --git a/resources/tools/dash/app/pal/templates/index_layout.jinja2 b/resources/tools/dash/app/pal/templates/index_layout.jinja2
deleted file mode 100644
index 4acd1bda2d..0000000000
--- a/resources/tools/dash/app/pal/templates/index_layout.jinja2
+++ /dev/null
@@ -1,34 +0,0 @@
-{% extends "base_layout.jinja2" %}
-
-{% block content %}
-<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
-
- <header class="mb-auto">
- <div>
- <h3 class="float-md-start mb-0 text-white">Dashboard</h3>
- </div>
- </header>
-
- <main class="px-3">
- <img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='img/logo.svg') }}" alt="" width="72" height="57">
- <h1 class="text-white">{{ title }}</h1>
- <p class="lead">{{ description }}</p>
- <p class="lead">
- <a href="/trending/" class="btn btn-primary fw-bold">Performance Trending</a>
- </p>
- <p class="lead">
- <a href="/report/" class="btn btn-primary fw-bold">Iterative Test Runs</a>
- </p>
- <p class="lead">
- <a href="/stats/" class="btn btn-primary fw-bold">Job Statistics</a>
- </p>
- <p class="lead">
- <a href="/news/" class="btn btn-primary fw-bold">News</a>
- </p>
- </main>
-
- <footer class="mt-auto text-white-50">
- <p>Copyright © 2016-2022 <a href="https://fd.io" class="text-white">The Fast Data Project</a>, a series of LF Projects, LLC.</p>
- </footer>
-</div>
-{% endblock %}
diff --git a/resources/tools/dash/app/pal/templates/news_layout.jinja2 b/resources/tools/dash/app/pal/templates/news_layout.jinja2
deleted file mode 100644
index c3ac89f731..0000000000
--- a/resources/tools/dash/app/pal/templates/news_layout.jinja2
+++ /dev/null
@@ -1,17 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <title>Continuous Performance News</title>
- {%metas%}
- {%favicon%}
- {%css%}
-</head>
-<body>
- {%app_entry%}
- <footer>
- {%config%}
- {%scripts%}
- {%renderer%}
- </footer>
-</body>
-</html> \ No newline at end of file
diff --git a/resources/tools/dash/app/pal/templates/report_layout.jinja2 b/resources/tools/dash/app/pal/templates/report_layout.jinja2
deleted file mode 100644
index c535d37b03..0000000000
--- a/resources/tools/dash/app/pal/templates/report_layout.jinja2
+++ /dev/null
@@ -1,17 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <title>Iterative Test Runs</title>
- {%metas%}
- {%favicon%}
- {%css%}
-</head>
-<body>
- {%app_entry%}
- <footer>
- {%config%}
- {%scripts%}
- {%renderer%}
- </footer>
-</body>
-</html> \ No newline at end of file
diff --git a/resources/tools/dash/app/pal/templates/stats_layout.jinja2 b/resources/tools/dash/app/pal/templates/stats_layout.jinja2
deleted file mode 100644
index dae6f00c19..0000000000
--- a/resources/tools/dash/app/pal/templates/stats_layout.jinja2
+++ /dev/null
@@ -1,17 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <title>Continuous Performance Statistics</title>
- {%metas%}
- {%favicon%}
- {%css%}
-</head>
-<body>
- {%app_entry%}
- <footer>
- {%config%}
- {%scripts%}
- {%renderer%}
- </footer>
-</body>
-</html> \ No newline at end of file
diff --git a/resources/tools/dash/app/pal/templates/trending_layout.jinja2 b/resources/tools/dash/app/pal/templates/trending_layout.jinja2
deleted file mode 100644
index 4881397cfd..0000000000
--- a/resources/tools/dash/app/pal/templates/trending_layout.jinja2
+++ /dev/null
@@ -1,17 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <title>Continuous Performance Trending</title>
- {%metas%}
- {%favicon%}
- {%css%}
-</head>
-<body>
- {%app_entry%}
- <footer>
- {%config%}
- {%scripts%}
- {%renderer%}
- </footer>
-</body>
-</html> \ No newline at end of file
diff --git a/resources/tools/dash/app/pal/trending/__init__.py b/resources/tools/dash/app/pal/trending/__init__.py
deleted file mode 100644
index 5692432123..0000000000
--- a/resources/tools/dash/app/pal/trending/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
diff --git a/resources/tools/dash/app/pal/trending/graphs.py b/resources/tools/dash/app/pal/trending/graphs.py
deleted file mode 100644
index 06bea25466..0000000000
--- a/resources/tools/dash/app/pal/trending/graphs.py
+++ /dev/null
@@ -1,417 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""
-"""
-
-import plotly.graph_objects as go
-import pandas as pd
-
-import hdrh.histogram
-import hdrh.codec
-
-from datetime import datetime
-
-from ..utils.constants import Constants as C
-from ..utils.utils import classify_anomalies, get_color
-
-
-def _get_hdrh_latencies(row: pd.Series, name: str) -> dict:
- """Get the HDRH latencies from the test data.
-
- :param row: A row fron the data frame with test data.
- :param name: The test name to be displayed as the graph title.
- :type row: pandas.Series
- :type name: str
- :returns: Dictionary with HDRH latencies.
- :rtype: dict
- """
-
- latencies = {"name": name}
- for key in C.LAT_HDRH:
- try:
- latencies[key] = row[key]
- except KeyError:
- return None
-
- return latencies
-
-
-def select_trending_data(data: pd.DataFrame, itm:dict) -> pd.DataFrame:
- """Select the data for graphs from the provided data frame.
-
- :param data: Data frame with data for graphs.
- :param itm: Item (in this case job name) which data will be selected from
- the input data frame.
- :type data: pandas.DataFrame
- :type itm: str
- :returns: A data frame with selected data.
- :rtype: pandas.DataFrame
- """
-
- phy = itm["phy"].split("-")
- if len(phy) == 4:
- topo, arch, nic, drv = phy
- if drv == "dpdk":
- drv = ""
- else:
- drv += "-"
- drv = drv.replace("_", "-")
- else:
- return None
-
- core = str() if itm["dut"] == "trex" else f"{itm['core']}"
- ttype = "ndrpdr" if itm["testtype"] in ("ndr", "pdr") else itm["testtype"]
- dut_v100 = "none" if itm["dut"] == "trex" else itm["dut"]
- dut_v101 = itm["dut"]
-
- df = data.loc[(
- (
- (
- (data["version"] == "1.0.0") &
- (data["dut_type"].str.lower() == dut_v100)
- ) |
- (
- (data["version"] == "1.0.1") &
- (data["dut_type"].str.lower() == dut_v101)
- )
- ) &
- (data["test_type"] == ttype) &
- (data["passed"] == True)
- )]
- df = df[df.job.str.endswith(f"{topo}-{arch}")]
- df = df[df.test_id.str.contains(
- f"^.*[.|-]{nic}.*{itm['framesize']}-{core}-{drv}{itm['test']}-{ttype}$",
- regex=True
- )].sort_values(by="start_time", ignore_index=True)
-
- return df
-
-
-def _generate_trending_traces(ttype: str, name: str, df: pd.DataFrame,
- start: datetime, end: datetime, color: str, norm_factor: float) -> list:
- """Generate the trending traces for the trending graph.
-
- :param ttype: Test type (MRR, NDR, PDR).
- :param name: The test name to be displayed as the graph title.
- :param df: Data frame with test data.
- :param start: The date (and time) when the selected data starts.
- :param end: The date (and time) when the selected data ends.
- :param color: The color of the trace (samples and trend line).
- :param norm_factor: The factor used for normalization of the results to CPU
- frequency set to Constants.NORM_FREQUENCY.
- :type ttype: str
- :type name: str
- :type df: pandas.DataFrame
- :type start: datetime.datetime
- :type end: datetime.datetime
- :type color: str
- :type norm_factor: float
- :returns: Traces (samples, trending line, anomalies)
- :rtype: list
- """
-
- df = df.dropna(subset=[C.VALUE[ttype], ])
- if df.empty:
- return list()
- df = df.loc[((df["start_time"] >= start) & (df["start_time"] <= end))]
- if df.empty:
- return list()
-
- x_axis = df["start_time"].tolist()
- if ttype == "pdr-lat":
- y_data = [(itm / norm_factor) for itm in df[C.VALUE[ttype]].tolist()]
- else:
- y_data = [(itm * norm_factor) for itm in df[C.VALUE[ttype]].tolist()]
-
- anomalies, trend_avg, trend_stdev = classify_anomalies(
- {k: v for k, v in zip(x_axis, y_data)}
- )
-
- hover = list()
- customdata = list()
- for idx, (_, row) in enumerate(df.iterrows()):
- d_type = "trex" if row["dut_type"] == "none" else row["dut_type"]
- hover_itm = (
- f"date: {row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>"
- f"<prop> [{row[C.UNIT[ttype]]}]: {y_data[idx]:,.0f}<br>"
- f"<stdev>"
- f"{d_type}-ref: {row['dut_version']}<br>"
- f"csit-ref: {row['job']}/{row['build']}<br>"
- f"hosts: {', '.join(row['hosts'])}"
- )
- if ttype == "mrr":
- stdev = (
- f"stdev [{row['result_receive_rate_rate_unit']}]: "
- f"{row['result_receive_rate_rate_stdev']:,.0f}<br>"
- )
- else:
- stdev = ""
- hover_itm = hover_itm.replace(
- "<prop>", "latency" if ttype == "pdr-lat" else "average"
- ).replace("<stdev>", stdev)
- hover.append(hover_itm)
- if ttype == "pdr-lat":
- customdata.append(_get_hdrh_latencies(row, name))
-
- hover_trend = list()
- for avg, stdev, (_, row) in zip(trend_avg, trend_stdev, df.iterrows()):
- d_type = "trex" if row["dut_type"] == "none" else row["dut_type"]
- hover_itm = (
- f"date: {row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>"
- f"trend [pps]: {avg:,.0f}<br>"
- f"stdev [pps]: {stdev:,.0f}<br>"
- f"{d_type}-ref: {row['dut_version']}<br>"
- f"csit-ref: {row['job']}/{row['build']}<br>"
- f"hosts: {', '.join(row['hosts'])}"
- )
- if ttype == "pdr-lat":
- hover_itm = hover_itm.replace("[pps]", "[us]")
- hover_trend.append(hover_itm)
-
- traces = [
- go.Scatter( # Samples
- x=x_axis,
- y=y_data,
- name=name,
- mode="markers",
- marker={
- "size": 5,
- "color": color,
- "symbol": "circle",
- },
- text=hover,
- hoverinfo="text+name",
- showlegend=True,
- legendgroup=name,
- customdata=customdata
- ),
- go.Scatter( # Trend line
- x=x_axis,
- y=trend_avg,
- name=name,
- mode="lines",
- line={
- "shape": "linear",
- "width": 1,
- "color": color,
- },
- text=hover_trend,
- hoverinfo="text+name",
- showlegend=False,
- legendgroup=name,
- )
- ]
-
- if anomalies:
- anomaly_x = list()
- anomaly_y = list()
- anomaly_color = list()
- hover = list()
- for idx, anomaly in enumerate(anomalies):
- if anomaly in ("regression", "progression"):
- anomaly_x.append(x_axis[idx])
- anomaly_y.append(trend_avg[idx])
- anomaly_color.append(C.ANOMALY_COLOR[anomaly])
- hover_itm = (
- f"date: {x_axis[idx].strftime('%Y-%m-%d %H:%M:%S')}<br>"
- f"trend [pps]: {trend_avg[idx]:,.0f}<br>"
- f"classification: {anomaly}"
- )
- if ttype == "pdr-lat":
- hover_itm = hover_itm.replace("[pps]", "[us]")
- hover.append(hover_itm)
- anomaly_color.extend([0.0, 0.5, 1.0])
- traces.append(
- go.Scatter(
- x=anomaly_x,
- y=anomaly_y,
- mode="markers",
- text=hover,
- hoverinfo="text+name",
- showlegend=False,
- legendgroup=name,
- name=name,
- marker={
- "size": 15,
- "symbol": "circle-open",
- "color": anomaly_color,
- "colorscale": C.COLORSCALE_LAT \
- if ttype == "pdr-lat" else C.COLORSCALE_TPUT,
- "showscale": True,
- "line": {
- "width": 2
- },
- "colorbar": {
- "y": 0.5,
- "len": 0.8,
- "title": "Circles Marking Data Classification",
- "titleside": "right",
- "tickmode": "array",
- "tickvals": [0.167, 0.500, 0.833],
- "ticktext": C.TICK_TEXT_LAT \
- if ttype == "pdr-lat" else C.TICK_TEXT_TPUT,
- "ticks": "",
- "ticklen": 0,
- "tickangle": -90,
- "thickness": 10
- }
- }
- )
- )
-
- return traces
-
-
-def graph_trending(data: pd.DataFrame, sel:dict, layout: dict,
- start: datetime, end: datetime, normalize: bool) -> tuple:
- """Generate the trending graph(s) - MRR, NDR, PDR and for PDR also Latences
- (result_latency_forward_pdr_50_avg).
-
- :param data: Data frame with test results.
- :param sel: Selected tests.
- :param layout: Layout of plot.ly graph.
- :param start: The date (and time) when the selected data starts.
- :param end: The date (and time) when the selected data ends.
- :param normalize: If True, the data is normalized to CPU frquency
- Constants.NORM_FREQUENCY.
- :type data: pandas.DataFrame
- :type sel: dict
- :type layout: dict
- :type start: datetime.datetime
- :type end: datetype.datetype
- :type normalize: bool
- :returns: Trending graph(s)
- :rtype: tuple(plotly.graph_objects.Figure, plotly.graph_objects.Figure)
- """
-
- if not sel:
- return None, None
-
- fig_tput = None
- fig_lat = None
- for idx, itm in enumerate(sel):
-
- df = select_trending_data(data, itm)
- if df is None or df.empty:
- continue
-
- name = "-".join((itm["dut"], itm["phy"], itm["framesize"], itm["core"],
- itm["test"], itm["testtype"], ))
- if normalize:
- phy = itm["phy"].split("-")
- topo_arch = f"{phy[0]}-{phy[1]}" if len(phy) == 4 else str()
- norm_factor = (C.NORM_FREQUENCY / C.FREQUENCY[topo_arch]) \
- if topo_arch else 1.0
- else:
- norm_factor = 1.0
- traces = _generate_trending_traces(
- itm["testtype"], name, df, start, end, get_color(idx), norm_factor
- )
- if traces:
- if not fig_tput:
- fig_tput = go.Figure()
- fig_tput.add_traces(traces)
-
- if itm["testtype"] == "pdr":
- traces = _generate_trending_traces(
- "pdr-lat", name, df, start, end, get_color(idx), norm_factor
- )
- if traces:
- if not fig_lat:
- fig_lat = go.Figure()
- fig_lat.add_traces(traces)
-
- if fig_tput:
- fig_tput.update_layout(layout.get("plot-trending-tput", dict()))
- if fig_lat:
- fig_lat.update_layout(layout.get("plot-trending-lat", dict()))
-
- return fig_tput, fig_lat
-
-
-def graph_hdrh_latency(data: dict, layout: dict) -> go.Figure:
- """Generate HDR Latency histogram graphs.
-
- :param data: HDRH data.
- :param layout: Layout of plot.ly graph.
- :type data: dict
- :type layout: dict
- :returns: HDR latency Histogram.
- :rtype: plotly.graph_objects.Figure
- """
-
- fig = None
-
- traces = list()
- for idx, (lat_name, lat_hdrh) in enumerate(data.items()):
- try:
- decoded = hdrh.histogram.HdrHistogram.decode(lat_hdrh)
- except (hdrh.codec.HdrLengthException, TypeError) as err:
- continue
- previous_x = 0.0
- prev_perc = 0.0
- xaxis = list()
- yaxis = list()
- hovertext = list()
- for item in decoded.get_recorded_iterator():
- # The real value is "percentile".
- # For 100%, we cut that down to "x_perc" to avoid
- # infinity.
- percentile = item.percentile_level_iterated_to
- x_perc = min(percentile, C.PERCENTILE_MAX)
- xaxis.append(previous_x)
- yaxis.append(item.value_iterated_to)
- hovertext.append(
- f"<b>{C.GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
- f"Direction: {('W-E', 'E-W')[idx % 2]}<br>"
- f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
- f"Latency: {item.value_iterated_to}uSec"
- )
- next_x = 100.0 / (100.0 - x_perc)
- xaxis.append(next_x)
- yaxis.append(item.value_iterated_to)
- hovertext.append(
- f"<b>{C.GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
- f"Direction: {('W-E', 'E-W')[idx % 2]}<br>"
- f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
- f"Latency: {item.value_iterated_to}uSec"
- )
- previous_x = next_x
- prev_perc = percentile
-
- traces.append(
- go.Scatter(
- x=xaxis,
- y=yaxis,
- name=C.GRAPH_LAT_HDRH_DESC[lat_name],
- mode="lines",
- legendgroup=C.GRAPH_LAT_HDRH_DESC[lat_name],
- showlegend=bool(idx % 2),
- line=dict(
- color=get_color(int(idx/2)),
- dash="solid",
- width=1 if idx % 2 else 2
- ),
- hovertext=hovertext,
- hoverinfo="text"
- )
- )
- if traces:
- fig = go.Figure()
- fig.add_traces(traces)
- layout_hdrh = layout.get("plot-hdrh-latency", None)
- if lat_hdrh:
- fig.update_layout(layout_hdrh)
-
- return fig
diff --git a/resources/tools/dash/app/pal/trending/layout.py b/resources/tools/dash/app/pal/trending/layout.py
deleted file mode 100644
index 2be19f8439..0000000000
--- a/resources/tools/dash/app/pal/trending/layout.py
+++ /dev/null
@@ -1,1393 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Plotly Dash HTML layout override.
-"""
-
-import logging
-import pandas as pd
-import dash_bootstrap_components as dbc
-
-from flask import Flask
-from dash import dcc
-from dash import html
-from dash import callback_context, no_update, ALL
-from dash import Input, Output, State
-from dash.exceptions import PreventUpdate
-from yaml import load, FullLoader, YAMLError
-from datetime import datetime, timedelta
-from copy import deepcopy
-from json import loads, JSONDecodeError
-from ast import literal_eval
-
-from ..utils.constants import Constants as C
-from ..utils.utils import show_tooltip, label, sync_checklists, list_tests, \
- get_date, gen_new_url
-from ..utils.url_processing import url_decode
-from ..data.data import Data
-from .graphs import graph_trending, graph_hdrh_latency, \
- select_trending_data
-
-
-class Layout:
- """The layout of the dash app and the callbacks.
- """
-
- def __init__(self, app: Flask, html_layout_file: str,
- graph_layout_file: str, data_spec_file: str, tooltip_file: str,
- time_period: str=None) -> None:
- """Initialization:
- - save the input parameters,
- - read and pre-process the data,
- - prepare data for the control panel,
- - read HTML layout file,
- - read tooltips from the tooltip file.
-
- :param app: Flask application running the dash application.
- :param html_layout_file: Path and name of the file specifying the HTML
- layout of the dash application.
- :param graph_layout_file: Path and name of the file with layout of
- plot.ly graphs.
- :param data_spec_file: Path and name of the file specifying the data to
- be read from parquets for this application.
- :param tooltip_file: Path and name of the yaml file specifying the
- tooltips.
- :param time_period: It defines the time period for data read from the
- parquets in days from now back to the past.
- :type app: Flask
- :type html_layout_file: str
- :type graph_layout_file: str
- :type data_spec_file: str
- :type tooltip_file: str
- :type time_period: int
- """
-
- # Inputs
- self._app = app
- self._html_layout_file = html_layout_file
- self._graph_layout_file = graph_layout_file
- self._data_spec_file = data_spec_file
- self._tooltip_file = tooltip_file
- self._time_period = time_period
-
- # Read the data:
- data_mrr = Data(
- data_spec_file=self._data_spec_file,
- debug=True
- ).read_trending_mrr(days=self._time_period)
-
- data_ndrpdr = Data(
- data_spec_file=self._data_spec_file,
- debug=True
- ).read_trending_ndrpdr(days=self._time_period)
-
- self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
-
- data_time_period = \
- (datetime.utcnow() - self._data["start_time"].min()).days
- if self._time_period > data_time_period:
- self._time_period = data_time_period
-
-
- # Get structure of tests:
- tbs = dict()
- for _, row in self._data[["job", "test_id"]].drop_duplicates().\
- iterrows():
- lst_job = row["job"].split("-")
- dut = lst_job[1]
- ttype = lst_job[3]
- tbed = "-".join(lst_job[-2:])
- lst_test = row["test_id"].split(".")
- if dut == "dpdk":
- area = "dpdk"
- else:
- area = "-".join(lst_test[3:-2])
- suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\
- replace("2n-", "")
- test = lst_test[-1]
- nic = suite.split("-")[0]
- for drv in C.DRIVERS:
- if drv in test:
- if drv == "af-xdp":
- driver = "af_xdp"
- else:
- driver = drv
- test = test.replace(f"{drv}-", "")
- break
- else:
- driver = "dpdk"
- infra = "-".join((tbed, nic, driver))
- lst_test = test.split("-")
- framesize = lst_test[0]
- core = lst_test[1] if lst_test[1] else "8C"
- test = "-".join(lst_test[2: -1])
-
- if tbs.get(dut, None) is None:
- tbs[dut] = dict()
- if tbs[dut].get(infra, None) is None:
- tbs[dut][infra] = dict()
- if tbs[dut][infra].get(area, None) is None:
- tbs[dut][infra][area] = dict()
- if tbs[dut][infra][area].get(test, None) is None:
- tbs[dut][infra][area][test] = dict()
- tbs[dut][infra][area][test]["core"] = list()
- tbs[dut][infra][area][test]["frame-size"] = list()
- tbs[dut][infra][area][test]["test-type"] = list()
- if core.upper() not in tbs[dut][infra][area][test]["core"]:
- tbs[dut][infra][area][test]["core"].append(core.upper())
- if framesize.upper() not in \
- tbs[dut][infra][area][test]["frame-size"]:
- tbs[dut][infra][area][test]["frame-size"].append(
- framesize.upper())
- if ttype == "mrr":
- if "MRR" not in tbs[dut][infra][area][test]["test-type"]:
- tbs[dut][infra][area][test]["test-type"].append("MRR")
- elif ttype == "ndrpdr":
- if "NDR" not in tbs[dut][infra][area][test]["test-type"]:
- tbs[dut][infra][area][test]["test-type"].extend(
- ("NDR", "PDR"))
- self._spec_tbs = tbs
-
- # Read from files:
- self._html_layout = ""
- self._graph_layout = None
- self._tooltips = dict()
-
- try:
- with open(self._html_layout_file, "r") as file_read:
- self._html_layout = file_read.read()
- except IOError as err:
- raise RuntimeError(
- f"Not possible to open the file {self._html_layout_file}\n{err}"
- )
-
- try:
- with open(self._graph_layout_file, "r") as file_read:
- self._graph_layout = load(file_read, Loader=FullLoader)
- except IOError as err:
- raise RuntimeError(
- f"Not possible to open the file {self._graph_layout_file}\n"
- f"{err}"
- )
- except YAMLError as err:
- raise RuntimeError(
- f"An error occurred while parsing the specification file "
- f"{self._graph_layout_file}\n{err}"
- )
-
- try:
- with open(self._tooltip_file, "r") as file_read:
- self._tooltips = load(file_read, Loader=FullLoader)
- except IOError as err:
- logging.warning(
- f"Not possible to open the file {self._tooltip_file}\n{err}"
- )
- except YAMLError as err:
- logging.warning(
- f"An error occurred while parsing the specification file "
- f"{self._tooltip_file}\n{err}"
- )
-
- # Callbacks:
- if self._app is not None and hasattr(self, 'callbacks'):
- self.callbacks(self._app)
-
- @property
- def html_layout(self):
- return self._html_layout
-
- @property
- def spec_tbs(self):
- return self._spec_tbs
-
- @property
- def data(self):
- return self._data
-
- @property
- def layout(self):
- return self._graph_layout
-
- @property
- def time_period(self):
- return self._time_period
-
- def add_content(self):
- """Top level method which generated the web page.
-
- It generates:
- - Store for user input data,
- - Navigation bar,
- - Main area with control panel and ploting area.
-
- If no HTML layout is provided, an error message is displayed instead.
-
- :returns: The HTML div with the whole page.
- :rtype: html.Div
- """
-
- if self.html_layout and self.spec_tbs:
- return html.Div(
- id="div-main",
- children=[
- dbc.Row(
- id="row-navbar",
- class_name="g-0",
- children=[
- self._add_navbar(),
- ]
- ),
- dcc.Loading(
- dbc.Offcanvas(
- class_name="w-50",
- id="offcanvas-metadata",
- title="Throughput And Latency",
- placement="end",
- is_open=False,
- children=[
- dbc.Row(id="metadata-tput-lat"),
- dbc.Row(id="metadata-hdrh-graph"),
- ]
- )
- ),
- dbc.Row(
- id="row-main",
- class_name="g-0",
- children=[
- dcc.Store(id="selected-tests"),
- dcc.Store(id="control-panel"),
- dcc.Location(id="url", refresh=False),
- self._add_ctrl_col(),
- self._add_plotting_col(),
- ]
- )
- ]
- )
- else:
- return html.Div(
- id="div-main-error",
- children=[
- dbc.Alert(
- [
- "An Error Occured",
- ],
- color="danger",
- ),
- ]
- )
-
- def _add_navbar(self):
- """Add nav element with navigation panel. It is placed on the top.
-
- :returns: Navigation bar.
- :rtype: dbc.NavbarSimple
- """
- return dbc.NavbarSimple(
- id="navbarsimple-main",
- children=[
- dbc.NavItem(
- dbc.NavLink(
- "Continuous Performance Trending",
- disabled=True,
- external_link=True,
- href="#"
- )
- )
- ],
- brand="Dashboard",
- brand_href="/",
- brand_external_link=True,
- class_name="p-2",
- fluid=True,
- )
-
- def _add_ctrl_col(self) -> dbc.Col:
- """Add column with controls. It is placed on the left side.
-
- :returns: Column with the control panel.
- :rtype: dbc.Col
- """
- return dbc.Col(
- id="col-controls",
- children=[
- self._add_ctrl_panel(),
- ],
- )
-
- def _add_plotting_col(self) -> dbc.Col:
- """Add column with plots and tables. It is placed on the right side.
-
- :returns: Column with tables.
- :rtype: dbc.Col
- """
- return dbc.Col(
- id="col-plotting-area",
- children=[
- dcc.Loading(
- children=[
- dbc.Row( # Throughput
- id="row-graph-tput",
- class_name="g-0 p-2",
- children=[
- C.PLACEHOLDER
- ]
- ),
- dbc.Row( # Latency
- id="row-graph-lat",
- class_name="g-0 p-2",
- children=[
- C.PLACEHOLDER
- ]
- ),
- dbc.Row( # Download
- id="row-btn-download",
- class_name="g-0 p-2",
- children=[
- C.PLACEHOLDER
- ]
- )
- ]
- )
- ],
- width=9,
- )
-
- def _add_ctrl_panel(self) -> dbc.Row:
- """Add control panel.
-
- :returns: Control panel.
- :rtype: dbc.Row
- """
- return dbc.Row(
- id="row-ctrl-panel",
- class_name="g-0 p-2",
- children=[
- dbc.Row(
- class_name="g-0",
- children=[
- dbc.InputGroup(
- [
- dbc.InputGroupText(
- children=show_tooltip(self._tooltips,
- "help-dut", "DUT")
- ),
- dbc.Select(
- id="dd-ctrl-dut",
- placeholder=(
- "Select a Device under Test..."
- ),
- options=sorted(
- [
- {"label": k, "value": k} \
- for k in self.spec_tbs.keys()
- ],
- key=lambda d: d["label"]
- )
- )
- ],
- class_name="mb-3",
- size="sm",
- ),
- ]
- ),
- dbc.Row(
- class_name="g-0",
- children=[
- dbc.InputGroup(
- [
- dbc.InputGroupText(
- children=show_tooltip(self._tooltips,
- "help-infra", "Infra")
- ),
- dbc.Select(
- id="dd-ctrl-phy",
- placeholder=(
- "Select a Physical Test Bed "
- "Topology..."
- )
- )
- ],
- class_name="mb-3",
- size="sm",
- ),
- ]
- ),
- dbc.Row(
- class_name="g-0",
- children=[
- dbc.InputGroup(
- [
- dbc.InputGroupText(
- children=show_tooltip(self._tooltips,
- "help-area", "Area")
- ),
- dbc.Select(
- id="dd-ctrl-area",
- placeholder="Select an Area...",
- disabled=True,
- ),
- ],
- class_name="mb-3",
- size="sm",
- ),
- ]
- ),
- dbc.Row(
- class_name="g-0",
- children=[
- dbc.InputGroup(
- [
- dbc.InputGroupText(
- children=show_tooltip(self._tooltips,
- "help-test", "Test")
- ),
- dbc.Select(
- id="dd-ctrl-test",
- placeholder="Select a Test...",
- disabled=True,
- ),
- ],
- class_name="mb-3",
- size="sm",
- ),
- ]
- ),
- dbc.Row(
- id="row-ctrl-framesize",
- class_name="gy-1",
- children=[
- dbc.Label(
- children=show_tooltip(self._tooltips,
- "help-framesize", "Frame Size"),
- class_name="p-0"
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-framesize-all",
- options=C.CL_ALL_DISABLED,
- inline=True,
- switch=False
- ),
- ],
- width=3
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-framesize",
- inline=True,
- switch=False
- )
- ]
- )
- ]
- ),
- dbc.Row(
- id="row-ctrl-core",
- class_name="gy-1",
- children=[
- dbc.Label(
- children=show_tooltip(self._tooltips,
- "help-cores", "Number of Cores"),
- class_name="p-0"
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-core-all",
- options=C.CL_ALL_DISABLED,
- inline=False,
- switch=False
- )
- ],
- width=3
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-core",
- inline=True,
- switch=False
- )
- ]
- )
- ]
- ),
- dbc.Row(
- id="row-ctrl-testtype",
- class_name="gy-1",
- children=[
- dbc.Label(
- children=show_tooltip(self._tooltips,
- "help-ttype", "Test Type"),
- class_name="p-0"
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-testtype-all",
- options=C.CL_ALL_DISABLED,
- inline=True,
- switch=False
- ),
- ],
- width=3
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-testtype",
- inline=True,
- switch=False
- )
- ]
- )
- ]
- ),
- dbc.Row(
- id="row-ctrl-normalize",
- class_name="gy-1",
- children=[
- dbc.Label(
- children=show_tooltip(self._tooltips,
- "help-normalize", "Normalize"),
- class_name="p-0"
- ),
- dbc.Col(
- children=[
- dbc.Checklist(
- id="cl-ctrl-normalize",
- options=[{
- "value": "normalize",
- "label": (
- "Normalize results to CPU"
- "frequency 2GHz"
- )
- }],
- value=[],
- inline=True,
- switch=False
- ),
- ]
- )
- ]
- ),
- dbc.Row(
- class_name="gy-1 p-0",
- children=[
- dbc.ButtonGroup(
- [
- dbc.Button(
- id="btn-ctrl-add",
- children="Add Selected",
- class_name="me-1",
- color="info"
- )
- ],
- size="md",
- )
- ]
- ),
- dbc.Row(
- class_name="gy-1",
- children=[
- dbc.Label(
- class_name="gy-1",
- children=show_tooltip(self._tooltips,
- "help-time-period", "Time Period"),
- ),
- dcc.DatePickerRange(
- id="dpr-period",
- className="d-flex justify-content-center",
- min_date_allowed=\
- datetime.utcnow() - timedelta(
- days=self.time_period),
- max_date_allowed=datetime.utcnow(),
- initial_visible_month=datetime.utcnow(),
- start_date=\
- datetime.utcnow() - timedelta(
- days=self.time_period),
- end_date=datetime.utcnow(),
- display_format="D MMM YY"
- )
- ]
- ),
- dbc.Row(
- id="row-card-sel-tests",
- class_name="gy-1",
- style=C.STYLE_DISABLED,
- children=[
- dbc.Label(
- "Selected tests",
- class_name="p-0"
- ),
- dbc.Checklist(
- class_name="overflow-auto",
- id="cl-selected",
- options=[],
- inline=False,
- style={"max-height": "12em"},
- )
- ],
- ),
- dbc.Row(
- id="row-btns-sel-tests",
- style=C.STYLE_DISABLED,
- children=[
- dbc.ButtonGroup(
- class_name="gy-2",
- children=[
- dbc.Button(
- id="btn-sel-remove",
- children="Remove Selected",
- class_name="w-100 me-1",
- color="info",
- disabled=False
- ),
- dbc.Button(
- id="btn-sel-remove-all",
- children="Remove All",
- class_name="w-100 me-1",
- color="info",
- disabled=False
- ),
- ],
- size="md",
- )
- ]
- ),
- ]
- )
-
- class ControlPanel:
- """A class representing the control panel.
- """
-
- def __init__(self, panel: dict) -> None:
- """Initialisation of the control pannel by default values. If
- particular values are provided (parameter "panel") they are set
- afterwards.
-
- :param panel: Custom values to be set to the control panel.
- :param default: Default values to be set to the control panel.
- :type panel: dict
- :type defaults: dict
- """
-
- # Defines also the order of keys
- self._defaults = {
- "dd-ctrl-dut-value": str(),
- "dd-ctrl-phy-options": list(),
- "dd-ctrl-phy-disabled": True,
- "dd-ctrl-phy-value": str(),
- "dd-ctrl-area-options": list(),
- "dd-ctrl-area-disabled": True,
- "dd-ctrl-area-value": str(),
- "dd-ctrl-test-options": list(),
- "dd-ctrl-test-disabled": True,
- "dd-ctrl-test-value": str(),
- "cl-ctrl-core-options": list(),
- "cl-ctrl-core-value": list(),
- "cl-ctrl-core-all-value": list(),
- "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
- "cl-ctrl-framesize-options": list(),
- "cl-ctrl-framesize-value": list(),
- "cl-ctrl-framesize-all-value": list(),
- "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
- "cl-ctrl-testtype-options": list(),
- "cl-ctrl-testtype-value": list(),
- "cl-ctrl-testtype-all-value": list(),
- "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
- "btn-ctrl-add-disabled": True,
- "cl-normalize-value": list(),
- "cl-selected-options": list(),
- }
-
- self._panel = deepcopy(self._defaults)
- if panel:
- for key in self._defaults:
- self._panel[key] = panel[key]
-
- @property
- def defaults(self) -> dict:
- return self._defaults
-
- @property
- def panel(self) -> dict:
- return self._panel
-
- def set(self, kwargs: dict) -> None:
- """Set the values of the Control panel.
-
- :param kwargs: key - value pairs to be set.
- :type kwargs: dict
- :raises KeyError: If the key in kwargs is not present in the Control
- panel.
- """
- for key, val in kwargs.items():
- if key in self._panel:
- self._panel[key] = val
- else:
- raise KeyError(f"The key {key} is not defined.")
-
- def get(self, key: str) -> any:
- """Returns the value of a key from the Control panel.
-
- :param key: The key which value should be returned.
- :type key: str
- :returns: The value of the key.
- :rtype: any
- :raises KeyError: If the key in kwargs is not present in the Control
- panel.
- """
- return self._panel[key]
-
- def values(self) -> tuple:
- """Returns the values from the Control panel as a list.
-
- :returns: The values from the Control panel.
- :rtype: list
- """
- return tuple(self._panel.values())
-
- def callbacks(self, app):
- """Callbacks for the whole application.
-
- :param app: The application.
- :type app: Flask
- """
-
- def _generate_plotting_area(figs: tuple, url: str) -> tuple:
- """Generate the plotting area with all its content.
-
- :param figs: Figures to be placed in the plotting area.
- :param utl: The URL to be placed in the plotting area bellow the
- tables.
- :type figs: tuple of plotly.graph_objects.Figure
- :type url: str
- :returns: tuple of elements to be shown in the plotting area.
- :rtype: tuple(dcc.Graph, dcc.Graph, list(dbc.Col, dbc.Col))
- """
-
- (fig_tput, fig_lat) = figs
-
- row_fig_tput = C.PLACEHOLDER
- row_fig_lat = C.PLACEHOLDER
- row_btn_dwnld = C.PLACEHOLDER
-
- if fig_tput:
- row_fig_tput = [
- dcc.Graph(
- id={"type": "graph", "index": "tput"},
- figure=fig_tput
- )
- ]
- row_btn_dwnld = [
- dbc.Col( # Download
- width=2,
- children=[
- dcc.Loading(children=[
- dbc.Button(
- id="btn-download-data",
- children=show_tooltip(self._tooltips,
- "help-download", "Download Data"),
- class_name="me-1",
- color="info"
- ),
- dcc.Download(id="download-data")
- ]),
- ]
- ),
- dbc.Col( # Show URL
- width=10,
- children=[
- dbc.InputGroup(
- class_name="me-1",
- children=[
- dbc.InputGroupText(
- style=C.URL_STYLE,
- children=show_tooltip(self._tooltips,
- "help-url", "URL", "input-url")
- ),
- dbc.Input(
- id="input-url",
- readonly=True,
- type="url",
- style=C.URL_STYLE,
- value=url
- )
- ]
- )
- ]
- )
- ]
- if fig_lat:
- row_fig_lat = [
- dcc.Graph(
- id={"type": "graph", "index": "lat"},
- figure=fig_lat
- )
- ]
-
- return row_fig_tput, row_fig_lat, row_btn_dwnld
-
- @app.callback(
- Output("control-panel", "data"), # Store
- Output("selected-tests", "data"), # Store
- Output("row-graph-tput", "children"),
- Output("row-graph-lat", "children"),
- Output("row-btn-download", "children"),
- Output("row-card-sel-tests", "style"),
- Output("row-btns-sel-tests", "style"),
- Output("dd-ctrl-dut", "value"),
- Output("dd-ctrl-phy", "options"),
- Output("dd-ctrl-phy", "disabled"),
- Output("dd-ctrl-phy", "value"),
- Output("dd-ctrl-area", "options"),
- Output("dd-ctrl-area", "disabled"),
- Output("dd-ctrl-area", "value"),
- Output("dd-ctrl-test", "options"),
- Output("dd-ctrl-test", "disabled"),
- Output("dd-ctrl-test", "value"),
- Output("cl-ctrl-core", "options"),
- Output("cl-ctrl-core", "value"),
- Output("cl-ctrl-core-all", "value"),
- Output("cl-ctrl-core-all", "options"),
- Output("cl-ctrl-framesize", "options"),
- Output("cl-ctrl-framesize", "value"),
- Output("cl-ctrl-framesize-all", "value"),
- Output("cl-ctrl-framesize-all", "options"),
- Output("cl-ctrl-testtype", "options"),
- Output("cl-ctrl-testtype", "value"),
- Output("cl-ctrl-testtype-all", "value"),
- Output("cl-ctrl-testtype-all", "options"),
- Output("btn-ctrl-add", "disabled"),
- Output("cl-ctrl-normalize", "value"),
- Output("cl-selected", "options"), # User selection
- State("control-panel", "data"), # Store
- State("selected-tests", "data"), # Store
- State("cl-selected", "value"), # User selection
- Input("dd-ctrl-dut", "value"),
- Input("dd-ctrl-phy", "value"),
- Input("dd-ctrl-area", "value"),
- Input("dd-ctrl-test", "value"),
- Input("cl-ctrl-core", "value"),
- Input("cl-ctrl-core-all", "value"),
- Input("cl-ctrl-framesize", "value"),
- Input("cl-ctrl-framesize-all", "value"),
- Input("cl-ctrl-testtype", "value"),
- Input("cl-ctrl-testtype-all", "value"),
- Input("cl-ctrl-normalize", "value"),
- Input("btn-ctrl-add", "n_clicks"),
- Input("dpr-period", "start_date"),
- Input("dpr-period", "end_date"),
- Input("btn-sel-remove", "n_clicks"),
- Input("btn-sel-remove-all", "n_clicks"),
- Input("url", "href")
- )
- def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
- dd_dut: str, dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
- cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
- cl_testtype: list, cl_testtype_all: list, cl_normalize: list,
- btn_add: int, d_start: str, d_end: str, btn_remove: int,
- btn_remove_all: int, href: str) -> tuple:
- """Update the application when the event is detected.
-
- :param cp_data: Current status of the control panel stored in
- browser.
- :param store_sel: List of tests selected by user stored in the
- browser.
- :param list_sel: List of tests selected by the user shown in the
- checklist.
- :param dd_dut: Input - DUTs.
- :param dd_phy: Input - topo- arch-nic-driver.
- :param dd_area: Input - Tested area.
- :param dd_test: Input - Test.
- :param cl_core: Input - Number of cores.
- :param cl_core_all: Input - All numbers of cores.
- :param cl_framesize: Input - Frame sizes.
- :param cl_framesize_all: Input - All frame sizes.
- :param cl_testtype: Input - Test type (NDR, PDR, MRR).
- :param cl_testtype_all: Input - All test types.
- :param cl_normalize: Input - Normalize the results.
- :param btn_add: Input - Button "Add Selected" tests.
- :param d_start: Date and time where the data processing starts.
- :param d_end: Date and time where the data processing ends.
- :param btn_remove: Input - Button "Remove selected" tests.
- :param btn_remove_all: Input - Button "Remove All" tests.
- :param href: Input - The URL provided by the browser.
- :type cp_data: dict
- :type store_sel: list
- :type list_sel: list
- :type dd_dut: str
- :type dd_phy: str
- :type dd_area: str
- :type dd_test: str
- :type cl_core: list
- :type cl_core_all: list
- :type cl_framesize: list
- :type cl_framesize_all: list
- :type cl_testtype: list
- :type cl_testtype_all: list
- :type cl_normalize: list
- :type btn_add: int
- :type d_start: str
- :type d_end: str
- :type btn_remove: int
- :type btn_remove_all: int
- :type href: str
- :returns: New values for web page elements.
- :rtype: tuple
- """
-
- ctrl_panel = self.ControlPanel(cp_data)
-
- d_start = get_date(d_start)
- d_end = get_date(d_end)
-
- # Parse the url:
- parsed_url = url_decode(href)
-
- row_fig_tput = no_update
- row_fig_lat = no_update
- row_btn_dwnld = no_update
- row_card_sel_tests = no_update
- row_btns_sel_tests = no_update
-
- trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
-
- if trigger_id == "dd-ctrl-dut":
- try:
- dut = self.spec_tbs[dd_dut]
- options = sorted(
- [{"label": v, "value": v}for v in dut.keys()],
- key=lambda d: d["label"]
- )
- disabled = False
- except KeyError:
- options = list()
- disabled = True
- ctrl_panel.set({
- "dd-ctrl-dut-value": dd_dut,
- "dd-ctrl-phy-value": str(),
- "dd-ctrl-phy-options": options,
- "dd-ctrl-phy-disabled": disabled,
- "dd-ctrl-area-value": str(),
- "dd-ctrl-area-options": list(),
- "dd-ctrl-area-disabled": True,
- "dd-ctrl-test-value": str(),
- "dd-ctrl-test-options": list(),
- "dd-ctrl-test-disabled": True,
- "cl-ctrl-core-options": list(),
- "cl-ctrl-core-value": list(),
- "cl-ctrl-core-all-value": list(),
- "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
- "cl-ctrl-framesize-options": list(),
- "cl-ctrl-framesize-value": list(),
- "cl-ctrl-framesize-all-value": list(),
- "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
- "cl-ctrl-testtype-options": list(),
- "cl-ctrl-testtype-value": list(),
- "cl-ctrl-testtype-all-value": list(),
- "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
- })
- elif trigger_id == "dd-ctrl-phy":
- try:
- dut = ctrl_panel.get("dd-ctrl-dut-value")
- phy = self.spec_tbs[dut][dd_phy]
- options = sorted(
- [{"label": label(v), "value": v} for v in phy.keys()],
- key=lambda d: d["label"]
- )
- disabled = False
- except KeyError:
- options = list()
- disabled = True
- ctrl_panel.set({
- "dd-ctrl-phy-value": dd_phy,
- "dd-ctrl-area-value": str(),
- "dd-ctrl-area-options": options,
- "dd-ctrl-area-disabled": disabled,
- "dd-ctrl-test-value": str(),
- "dd-ctrl-test-options": list(),
- "dd-ctrl-test-disabled": True,
- "cl-ctrl-core-options": list(),
- "cl-ctrl-core-value": list(),
- "cl-ctrl-core-all-value": list(),
- "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
- "cl-ctrl-framesize-options": list(),
- "cl-ctrl-framesize-value": list(),
- "cl-ctrl-framesize-all-value": list(),
- "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
- "cl-ctrl-testtype-options": list(),
- "cl-ctrl-testtype-value": list(),
- "cl-ctrl-testtype-all-value": list(),
- "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
- })
- elif trigger_id == "dd-ctrl-area":
- try:
- dut = ctrl_panel.get("dd-ctrl-dut-value")
- phy = ctrl_panel.get("dd-ctrl-phy-value")
- area = self.spec_tbs[dut][phy][dd_area]
- options = sorted(
- [{"label": v, "value": v} for v in area.keys()],
- key=lambda d: d["label"]
- )
- disabled = False
- except KeyError:
- options = list()
- disabled = True
- ctrl_panel.set({
- "dd-ctrl-area-value": dd_area,
- "dd-ctrl-test-value": str(),
- "dd-ctrl-test-options": options,
- "dd-ctrl-test-disabled": disabled,
- "cl-ctrl-core-options": list(),
- "cl-ctrl-core-value": list(),
- "cl-ctrl-core-all-value": list(),
- "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
- "cl-ctrl-framesize-options": list(),
- "cl-ctrl-framesize-value": list(),
- "cl-ctrl-framesize-all-value": list(),
- "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
- "cl-ctrl-testtype-options": list(),
- "cl-ctrl-testtype-value": list(),
- "cl-ctrl-testtype-all-value": list(),
- "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
- })
- elif trigger_id == "dd-ctrl-test":
- core_opts = list()
- framesize_opts = list()
- testtype_opts = list()
- dut = ctrl_panel.get("dd-ctrl-dut-value")
- phy = ctrl_panel.get("dd-ctrl-phy-value")
- area = ctrl_panel.get("dd-ctrl-area-value")
- test = self.spec_tbs[dut][phy][area][dd_test]
- cores = test["core"]
- fsizes = test["frame-size"]
- ttypes = test["test-type"]
- if dut and phy and area and dd_test:
- core_opts = [{"label": v, "value": v}
- for v in sorted(cores)]
- framesize_opts = [{"label": v, "value": v}
- for v in sorted(fsizes)]
- testtype_opts = [{"label": v, "value": v}
- for v in sorted(ttypes)]
- ctrl_panel.set({
- "dd-ctrl-test-value": dd_test,
- "cl-ctrl-core-options": core_opts,
- "cl-ctrl-core-value": list(),
- "cl-ctrl-core-all-value": list(),
- "cl-ctrl-core-all-options": C.CL_ALL_ENABLED,
- "cl-ctrl-framesize-options": framesize_opts,
- "cl-ctrl-framesize-value": list(),
- "cl-ctrl-framesize-all-value": list(),
- "cl-ctrl-framesize-all-options": C.CL_ALL_ENABLED,
- "cl-ctrl-testtype-options": testtype_opts,
- "cl-ctrl-testtype-value": list(),
- "cl-ctrl-testtype-all-value": list(),
- "cl-ctrl-testtype-all-options": C.CL_ALL_ENABLED,
- })
- elif trigger_id == "cl-ctrl-core":
- val_sel, val_all = sync_checklists(
- options=ctrl_panel.get("cl-ctrl-core-options"),
- sel=cl_core,
- all=list(),
- id=""
- )
- ctrl_panel.set({
- "cl-ctrl-core-value": val_sel,
- "cl-ctrl-core-all-value": val_all,
- })
- elif trigger_id == "cl-ctrl-core-all":
- val_sel, val_all = sync_checklists(
- options = ctrl_panel.get("cl-ctrl-core-options"),
- sel=list(),
- all=cl_core_all,
- id="all"
- )
- ctrl_panel.set({
- "cl-ctrl-core-value": val_sel,
- "cl-ctrl-core-all-value": val_all,
- })
- elif trigger_id == "cl-ctrl-framesize":
- val_sel, val_all = sync_checklists(
- options = ctrl_panel.get("cl-ctrl-framesize-options"),
- sel=cl_framesize,
- all=list(),
- id=""
- )
- ctrl_panel.set({
- "cl-ctrl-framesize-value": val_sel,
- "cl-ctrl-framesize-all-value": val_all,
- })
- elif trigger_id == "cl-ctrl-framesize-all":
- val_sel, val_all = sync_checklists(
- options = ctrl_panel.get("cl-ctrl-framesize-options"),
- sel=list(),
- all=cl_framesize_all,
- id="all"
- )
- ctrl_panel.set({
- "cl-ctrl-framesize-value": val_sel,
- "cl-ctrl-framesize-all-value": val_all,
- })
- elif trigger_id == "cl-ctrl-testtype":
- val_sel, val_all = sync_checklists(
- options = ctrl_panel.get("cl-ctrl-testtype-options"),
- sel=cl_testtype,
- all=list(),
- id=""
- )
- ctrl_panel.set({
- "cl-ctrl-testtype-value": val_sel,
- "cl-ctrl-testtype-all-value": val_all,
- })
- elif trigger_id == "cl-ctrl-testtype-all":
- val_sel, val_all = sync_checklists(
- options = ctrl_panel.get("cl-ctrl-testtype-options"),
- sel=list(),
- all=cl_testtype_all,
- id="all"
- )
- ctrl_panel.set({
- "cl-ctrl-testtype-value": val_sel,
- "cl-ctrl-testtype-all-value": val_all,
- })
- elif trigger_id == "btn-ctrl-add":
- _ = btn_add
- dut = ctrl_panel.get("dd-ctrl-dut-value")
- phy = ctrl_panel.get("dd-ctrl-phy-value")
- area = ctrl_panel.get("dd-ctrl-area-value")
- test = ctrl_panel.get("dd-ctrl-test-value")
- cores = ctrl_panel.get("cl-ctrl-core-value")
- framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
- testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
- # Add selected test to the list of tests in store:
- if all((dut, phy, area, test, cores, framesizes, testtypes)):
- if store_sel is None:
- store_sel = list()
- for core in cores:
- for framesize in framesizes:
- for ttype in testtypes:
- if dut == "trex":
- core = str()
- tid = "-".join((
- dut, phy.replace('af_xdp', 'af-xdp'), area,
- framesize.lower(), core.lower(), test,
- ttype.lower()
- ))
- if tid not in [itm["id"] for itm in store_sel]:
- store_sel.append({
- "id": tid,
- "dut": dut,
- "phy": phy,
- "area": area,
- "test": test,
- "framesize": framesize.lower(),
- "core": core.lower(),
- "testtype": ttype.lower()
- })
- store_sel = sorted(store_sel, key=lambda d: d["id"])
- row_card_sel_tests = C.STYLE_ENABLED
- row_btns_sel_tests = C.STYLE_ENABLED
- if C.CLEAR_ALL_INPUTS:
- ctrl_panel.set(ctrl_panel.defaults)
- elif trigger_id == "btn-sel-remove-all":
- _ = btn_remove_all
- row_fig_tput = C.PLACEHOLDER
- row_fig_lat = C.PLACEHOLDER
- row_btn_dwnld = C.PLACEHOLDER
- row_card_sel_tests = C.STYLE_DISABLED
- row_btns_sel_tests = C.STYLE_DISABLED
- store_sel = list()
- ctrl_panel.set({"cl-selected-options": list()})
- elif trigger_id == "btn-sel-remove":
- _ = btn_remove
- if list_sel:
- new_store_sel = list()
- for item in store_sel:
- if item["id"] not in list_sel:
- new_store_sel.append(item)
- store_sel = new_store_sel
- elif trigger_id == "url":
- # TODO: Add verification
- url_params = parsed_url["params"]
- if url_params:
- store_sel = literal_eval(
- url_params.get("store_sel", list())[0])
- d_start = get_date(url_params.get("start", list())[0])
- d_end = get_date(url_params.get("end", list())[0])
- if store_sel:
- row_card_sel_tests = C.STYLE_ENABLED
- row_btns_sel_tests = C.STYLE_ENABLED
-
- if trigger_id in ("btn-ctrl-add", "url", "dpr-period",
- "btn-sel-remove", "cl-ctrl-normalize"):
- if store_sel:
- row_fig_tput, row_fig_lat, row_btn_dwnld = \
- _generate_plotting_area(
- graph_trending(self.data, store_sel, self.layout,
- d_start, d_end, bool(cl_normalize)),
- gen_new_url(
- parsed_url,
- {
- "store_sel": store_sel,
- "start": d_start,
- "end": d_end
- }
- )
- )
- ctrl_panel.set({
- "cl-selected-options": list_tests(store_sel)
- })
- else:
- row_fig_tput = C.PLACEHOLDER
- row_fig_lat = C.PLACEHOLDER
- row_btn_dwnld = C.PLACEHOLDER
- row_card_sel_tests = C.STYLE_DISABLED
- row_btns_sel_tests = C.STYLE_DISABLED
- store_sel = list()
- ctrl_panel.set({"cl-selected-options": list()})
-
- if ctrl_panel.get("cl-ctrl-core-value") and \
- ctrl_panel.get("cl-ctrl-framesize-value") and \
- ctrl_panel.get("cl-ctrl-testtype-value"):
- disabled = False
- else:
- disabled = True
- ctrl_panel.set({
- "btn-ctrl-add-disabled": disabled,
- "cl-normalize-value": cl_normalize
- })
-
- ret_val = [
- ctrl_panel.panel, store_sel,
- row_fig_tput, row_fig_lat, row_btn_dwnld,
- row_card_sel_tests, row_btns_sel_tests
- ]
- ret_val.extend(ctrl_panel.values())
- return ret_val
-
- @app.callback(
- Output("metadata-tput-lat", "children"),
- Output("metadata-hdrh-graph", "children"),
- Output("offcanvas-metadata", "is_open"),
- Input({"type": "graph", "index": ALL}, "clickData"),
- prevent_initial_call=True
- )
- def _show_metadata_from_graphs(graph_data: dict) -> tuple:
- """Generates the data for the offcanvas displayed when a particular
- point in a graph is clicked on.
-
- :param graph_data: The data from the clicked point in the graph.
- :type graph_data: dict
- :returns: The data to be displayed on the offcanvas and the
- information to show the offcanvas.
- :rtype: tuple(list, list, bool)
- """
- try:
- trigger_id = loads(
- callback_context.triggered[0]["prop_id"].split(".")[0]
- )["index"]
- idx = 0 if trigger_id == "tput" else 1
- graph_data = graph_data[idx]["points"][0]
- except (JSONDecodeError, IndexError, KeyError, ValueError,
- TypeError):
- raise PreventUpdate
-
- metadata = no_update
- graph = list()
-
- children = [
- dbc.ListGroupItem(
- [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
- ) for x in graph_data.get("text", "").split("<br>")
- ]
- if trigger_id == "tput":
- title = "Throughput"
- elif trigger_id == "lat":
- title = "Latency"
- hdrh_data = graph_data.get("customdata", None)
- if hdrh_data:
- graph = [dbc.Card(
- class_name="gy-2 p-0",
- children=[
- dbc.CardHeader(hdrh_data.pop("name")),
- dbc.CardBody(children=[
- dcc.Graph(
- id="hdrh-latency-graph",
- figure=graph_hdrh_latency(
- hdrh_data, self.layout
- )
- )
- ])
- ])
- ]
- metadata = [
- dbc.Card(
- class_name="gy-2 p-0",
- children=[
- dbc.CardHeader(children=[
- dcc.Clipboard(
- target_id="tput-lat-metadata",
- title="Copy",
- style={"display": "inline-block"}
- ),
- title
- ]),
- dbc.CardBody(
- id="tput-lat-metadata",
- class_name="p-0",
- children=[dbc.ListGroup(children, flush=True), ]
- )
- ]
- )
- ]
-
- return metadata, graph, True
-
- @app.callback(
- Output("download-data", "data"),
- State("selected-tests", "data"),
- Input("btn-download-data", "n_clicks"),
- prevent_initial_call=True
- )
- def _download_data(store_sel, n_clicks):
- """Download the data
-
- :param store_sel: List of tests selected by user stored in the
- browser.
- :param n_clicks: Number of clicks on the button "Download".
- :type store_sel: list
- :type n_clicks: int
- :returns: dict of data frame content (base64 encoded) and meta data
- used by the Download component.
- :rtype: dict
- """
-
- if not n_clicks:
- raise PreventUpdate
-
- if not store_sel:
- raise PreventUpdate
-
- df = pd.DataFrame()
- for itm in store_sel:
- sel_data = select_trending_data(self.data, itm)
- if sel_data is None:
- continue
- df = pd.concat([df, sel_data], ignore_index=True)
-
- return dcc.send_data_frame(df.to_csv, C.TREND_DOWNLOAD_FILE_NAME)
diff --git a/resources/tools/dash/app/pal/trending/layout.yaml b/resources/tools/dash/app/pal/trending/layout.yaml
deleted file mode 100644
index 0c0b62d591..0000000000
--- a/resources/tools/dash/app/pal/trending/layout.yaml
+++ /dev/null
@@ -1,210 +0,0 @@
-plot-trending-tput:
- # title: ""
- # titlefont:
- # size: 16
- autosize: True
- showlegend: True
- # width: 1100
- #height: 400
- yaxis:
- showticklabels: True
- tickformat: ".3s"
- title: "Throughput [Mpps]"
- hoverformat: ".5s"
- gridcolor: "rgb(238, 238, 238)"
- linecolor: "rgb(238, 238, 238)"
- showline: True
- zeroline: False
- tickcolor: "rgb(238, 238, 238)"
- linewidth: 1
- showgrid: True
- xaxis:
- title: 'Date [MMDD]'
- type: "date"
- autorange: True
- fixedrange: False
- showgrid: True
- gridcolor: "rgb(238, 238, 238)"
- showline: True
- linecolor: "rgb(238, 238, 238)"
- zeroline: False
- linewidth: 1
- showticklabels: True
- tickcolor: "rgb(238, 238, 238)"
- tickmode: "auto"
- tickformat: "%m%d"
- rangeselector:
- buttons:
- - count: 14
- label: "2w"
- step: "day"
- stepmode: "backward"
- - count: 1
- label: "1m"
- step: "month"
- stepmode: "backward"
- - count: 2
- label: "2m"
- step: "month"
- stepmode: "backward"
- - count: 3
- label: "3m"
- step: "month"
- stepmode: "backward"
- - count: 4
- label: "4m"
- step: "month"
- stepmode: "backward"
- - count: 5
- label: "5m"
- step: "month"
- stepmode: "backward"
- - step: "all"
- margin:
- r: 20
- b: 0
- t: 5
- l: 70
- legend:
- orientation: "h"
- y: -0.18
- xanchor: "auto"
- traceorder: "normal"
- bordercolor: "rgb(238, 238, 238)"
- paper_bgcolor: "#fff"
- plot_bgcolor: "#fff"
- hoverlabel:
- namelength: -1
-
-plot-trending-lat:
- # title: ""
- # titlefont:
- # size: 16
- autosize: True
- showlegend: True
- # width: 1100
- #height: 400
- yaxis:
- showticklabels: True
- tickformat: ".3s"
- title: "Average Latency at 50% PDR [us]"
- hoverformat: ".5s"
- gridcolor: "rgb(238, 238, 238)"
- linecolor: "rgb(238, 238, 238)"
- showline: True
- zeroline: False
- tickcolor: "rgb(238, 238, 238)"
- linewidth: 1
- showgrid: True
- xaxis:
- title: 'Date [MMDD]'
- type: "date"
- autorange: True
- fixedrange: False
- showgrid: True
- gridcolor: "rgb(238, 238, 238)"
- showline: True
- linecolor: "rgb(238, 238, 238)"
- zeroline: False
- linewidth: 1
- showticklabels: True
- tickcolor: "rgb(238, 238, 238)"
- tickmode: "auto"
- tickformat: "%m%d"
- rangeselector:
- buttons:
- - count: 14
- label: "2w"
- step: "day"
- stepmode: "backward"
- - count: 1
- label: "1m"
- step: "month"
- stepmode: "backward"
- - count: 2
- label: "2m"
- step: "month"
- stepmode: "backward"
- - count: 3
- label: "3m"
- step: "month"
- stepmode: "backward"
- - count: 4
- label: "4m"
- step: "month"
- stepmode: "backward"
- - count: 5
- label: "5m"
- step: "month"
- stepmode: "backward"
- - step: "all"
- margin:
- r: 20
- b: 0
- t: 5
- l: 70
- legend:
- orientation: "h"
- y: -0.18
- xanchor: "auto"
- traceorder: "normal"
- bordercolor: "rgb(238, 238, 238)"
- paper_bgcolor: "#fff"
- plot_bgcolor: "#fff"
- hoverlabel:
- namelength: -1
-
-plot-hdrh-latency:
- # title:
- # text: "Latency by Percentile Distribution"
- # xanchor: "center"
- # x: 0.5
- # font:
- # size: 10
- showlegend: True
- legend:
- traceorder: "normal"
- orientation: "h"
- # font:
- # size: 16
- xanchor: "left"
- yanchor: "top"
- x: 0
- y: -0.25
- bgcolor: "rgba(255, 255, 255, 0)"
- bordercolor: "rgba(255, 255, 255, 0)"
- xaxis:
- type: "log"
- title: "Percentile [%]"
- # titlefont:
- # size: 14
- autorange: False
- fixedrange: True
- gridcolor: "rgb(230, 230, 230)"
- linecolor: "rgb(220, 220, 220)"
- linewidth: 1
- showgrid: True
- showline: True
- showticklabels: True
- tickcolor: "rgb(220, 220, 220)"
- tickvals: [1, 2, 1e1, 20, 1e2, 1e3, 1e4, 1e5, 1e6]
- ticktext: [0, 50, 90, 95, 99, 99.9, 99.99, 99.999, 99.9999]
- # tickfont:
- # size: 14
- yaxis:
- title: "One-Way Latency per Direction [us]"
- # titlefont:
- # size: 14
- gridcolor: "rgb(230, 230, 230)"
- linecolor: "rgb(220, 220, 220)"
- linewidth: 1
- showgrid: True
- showline: True
- showticklabels: True
- tickcolor: "rgb(220, 220, 220)"
- # tickfont:
- # size: 14
- autosize: True
- #height: 400
- paper_bgcolor: "white"
- plot_bgcolor: "white"
diff --git a/resources/tools/dash/app/pal/trending/trending.py b/resources/tools/dash/app/pal/trending/trending.py
deleted file mode 100644
index af1dc79722..0000000000
--- a/resources/tools/dash/app/pal/trending/trending.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Instantiate the Trending Dash application.
-"""
-import dash
-
-from ..utils.constants import Constants as C
-from .layout import Layout
-
-
-def init_trending(server, time_period=None):
- """Create a Plotly Dash dashboard.
-
- :param server: Flask server.
- :type server: Flask
- :returns: Dash app server.
- :rtype: Dash
- """
-
- dash_app = dash.Dash(
- server=server,
- routes_pathname_prefix=C.TREND_ROUTES_PATHNAME_PREFIX,
- external_stylesheets=C.EXTERNAL_STYLESHEETS
- )
-
- layout = Layout(
- app=dash_app,
- html_layout_file=C.TREND_HTML_LAYOUT_FILE,
- graph_layout_file=C.TREND_GRAPH_LAYOUT_FILE,
- data_spec_file=C.DATA_SPEC_FILE,
- tooltip_file=C.TOOLTIP_FILE,
- time_period=time_period
- )
- dash_app.index_string = layout.html_layout
- dash_app.layout = layout.add_content()
-
- return dash_app.server
diff --git a/resources/tools/dash/app/pal/utils/__init__.py b/resources/tools/dash/app/pal/utils/__init__.py
deleted file mode 100644
index 5692432123..0000000000
--- a/resources/tools/dash/app/pal/utils/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
diff --git a/resources/tools/dash/app/pal/utils/constants.py b/resources/tools/dash/app/pal/utils/constants.py
deleted file mode 100644
index cc4a9e0f23..0000000000
--- a/resources/tools/dash/app/pal/utils/constants.py
+++ /dev/null
@@ -1,312 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Constants used in Dash PAL.
-
-"Constant" means a value that keeps its value since initialization. The value
-does not need to be hard coded here, but can be read from environment variables.
-"""
-
-
-import dash_bootstrap_components as dbc
-
-from dash import html
-
-
-class Constants:
- """Constants used in Dash PAL.
- """
-
- ############################################################################
- # General, application wide constants.
-
- # The application title.
- TITLE = "FD.io CSIT"
-
- # The application description.
- DESCRIPTION = "Performance Dashboard"
-
- # External stylesheets.
- EXTERNAL_STYLESHEETS = [dbc.themes.LUX, ]
-
- # Top level template for all pages.
- TEMPLATE = "d-flex h-100 text-center text-white bg-dark"
-
- # Path and name of the file specifying the HTML layout of the dash
- # application.
- MAIN_HTML_LAYOUT_FILE = "index_layout.jinja2"
-
- # Application root.
- APPLICATIN_ROOT = "/"
-
- # Data to be downloaded from the parquets specification file.
- DATA_SPEC_FILE = "pal/data/data.yaml"
-
- # The file with tooltips.
- TOOLTIP_FILE = "pal/utils/tooltips.yaml"
-
- # Maximal value of TIME_PERIOD for data read from the parquets in days.
- # Do not change without a good reason.
- MAX_TIME_PERIOD = 180
-
- # It defines the time period for data read from the parquets in days from
- # now back to the past.
- # TIME_PERIOD = None - means all data (max MAX_TIME_PERIOD days) is read.
- # TIME_PERIOD = MAX_TIME_PERIOD - is the default value
- TIME_PERIOD = MAX_TIME_PERIOD # [days]
-
- # List of releases used for iterative data processing.
- # The releases MUST be in the order from the current (newest) to the last
- # (oldest).
- RELEASES = ["csit2206", "csit2202", ]
-
- ############################################################################
- # General, application wide, layout affecting constants.
-
- # If True, clear all inputs in control panel when button "ADD SELECTED" is
- # pressed.
- CLEAR_ALL_INPUTS = False
-
- # The element is disabled.
- STYLE_DISABLED = {"display": "none"}
-
- # The element is enabled and visible.
- STYLE_ENABLED = {"display": "inherit"}
-
- # Checklist "All" is disabled.
- CL_ALL_DISABLED = [
- {
- "label": "All",
- "value": "all",
- "disabled": True
- }
- ]
-
- # Checklist "All" is enable, visible and unchecked.
- CL_ALL_ENABLED = [
- {
- "label": "All",
- "value": "all",
- "disabled": False
- }
- ]
-
- # Placeholder for any element in the layout.
- PLACEHOLDER = html.Nobr("")
-
- # List of drivers used in CSIT.
- DRIVERS = ("avf", "af-xdp", "rdma", "dpdk")
-
- # Labels for input elements (dropdowns, ...).
- LABELS = {
- "dpdk": "DPDK",
- "container_memif": "LXC/DRC Container Memif",
- "crypto": "IPSec IPv4 Routing",
- "ip4": "IPv4 Routing",
- "ip6": "IPv6 Routing",
- "ip4_tunnels": "IPv4 Tunnels",
- "l2": "L2 Ethernet Switching",
- "srv6": "SRv6 Routing",
- "vm_vhost": "VMs vhost-user",
- "nfv_density-dcr_memif-chain_ipsec": "CNF Service Chains Routing IPSec",
- "nfv_density-vm_vhost-chain_dot1qip4vxlan":"VNF Service Chains Tunnels",
- "nfv_density-vm_vhost-chain": "VNF Service Chains Routing",
- "nfv_density-dcr_memif-pipeline": "CNF Service Pipelines Routing",
- "nfv_density-dcr_memif-chain": "CNF Service Chains Routing",
- }
-
- # URL style.
- URL_STYLE = {
- "background-color": "#d2ebf5",
- "border-color": "#bce1f1",
- "color": "#135d7c"
- }
-
- ############################################################################
- # General, normalization constants.
-
- NORM_FREQUENCY = 2.0 # [GHz]
- FREQUENCY = { # [GHz]
- "2n-aws": 1.000,
- "2n-dnv": 2.000,
- "2n-clx": 2.300,
- "2n-icx": 2.600,
- "2n-skx": 2.500,
- "2n-tx2": 2.500,
- "2n-zn2": 2.900,
- "3n-alt": 3.000,
- "3n-aws": 1.000,
- "3n-dnv": 2.000,
- "3n-icx": 2.600,
- "3n-skx": 2.500,
- "3n-tsh": 2.200
- }
-
- ############################################################################
- # General, plots constants.
-
- PLOT_COLORS = (
- "#1A1110", "#DA2647", "#214FC6", "#01786F", "#BD8260", "#FFD12A",
- "#A6E7FF", "#738276", "#C95A49", "#FC5A8D", "#CEC8EF", "#391285",
- "#6F2DA8", "#FF878D", "#45A27D", "#FFD0B9", "#FD5240", "#DB91EF",
- "#44D7A8", "#4F86F7", "#84DE02", "#FFCFF1", "#614051"
- )
-
- # Trending, anomalies.
- ANOMALY_COLOR = {
- "regression": 0.0,
- "normal": 0.5,
- "progression": 1.0
- }
-
- COLORSCALE_TPUT = [
- [0.00, "red"],
- [0.33, "red"],
- [0.33, "white"],
- [0.66, "white"],
- [0.66, "green"],
- [1.00, "green"]
- ]
-
- TICK_TEXT_TPUT = ["Regression", "Normal", "Progression"]
-
- COLORSCALE_LAT = [
- [0.00, "green"],
- [0.33, "green"],
- [0.33, "white"],
- [0.66, "white"],
- [0.66, "red"],
- [1.00, "red"]
- ]
-
- TICK_TEXT_LAT = ["Progression", "Normal", "Regression"]
-
- # Access to the results.
- VALUE = {
- "mrr": "result_receive_rate_rate_avg",
- "ndr": "result_ndr_lower_rate_value",
- "pdr": "result_pdr_lower_rate_value",
- "pdr-lat": "result_latency_forward_pdr_50_avg"
- }
-
- VALUE_ITER = {
- "mrr": "result_receive_rate_rate_values",
- "ndr": "result_ndr_lower_rate_value",
- "pdr": "result_pdr_lower_rate_value",
- "pdr-lat": "result_latency_forward_pdr_50_avg"
- }
-
- UNIT = {
- "mrr": "result_receive_rate_rate_unit",
- "ndr": "result_ndr_lower_rate_unit",
- "pdr": "result_pdr_lower_rate_unit",
- "pdr-lat": "result_latency_forward_pdr_50_unit"
- }
-
- # Latencies.
- LAT_HDRH = ( # Do not change the order
- "result_latency_forward_pdr_0_hdrh",
- "result_latency_reverse_pdr_0_hdrh",
- "result_latency_forward_pdr_10_hdrh",
- "result_latency_reverse_pdr_10_hdrh",
- "result_latency_forward_pdr_50_hdrh",
- "result_latency_reverse_pdr_50_hdrh",
- "result_latency_forward_pdr_90_hdrh",
- "result_latency_reverse_pdr_90_hdrh",
- )
-
- # This value depends on latency stream rate (9001 pps) and duration (5s).
- # Keep it slightly higher to ensure rounding errors to not remove tick mark.
- PERCENTILE_MAX = 99.999501
-
- GRAPH_LAT_HDRH_DESC = {
- "result_latency_forward_pdr_0_hdrh": "No-load.",
- "result_latency_reverse_pdr_0_hdrh": "No-load.",
- "result_latency_forward_pdr_10_hdrh": "Low-load, 10% PDR.",
- "result_latency_reverse_pdr_10_hdrh": "Low-load, 10% PDR.",
- "result_latency_forward_pdr_50_hdrh": "Mid-load, 50% PDR.",
- "result_latency_reverse_pdr_50_hdrh": "Mid-load, 50% PDR.",
- "result_latency_forward_pdr_90_hdrh": "High-load, 90% PDR.",
- "result_latency_reverse_pdr_90_hdrh": "High-load, 90% PDR."
- }
-
- ############################################################################
- # News.
-
- # The pathname prefix for the application.
- NEWS_ROUTES_PATHNAME_PREFIX = "/news/"
-
- # Path and name of the file specifying the HTML layout of the dash
- # application.
- NEWS_HTML_LAYOUT_FILE = "pal/templates/news_layout.jinja2"
-
- # The default job displayed when the page is loaded first time.
- NEWS_DEFAULT_JOB = "csit-vpp-perf-mrr-daily-master-2n-icx"
-
- # Time period for regressions and progressions. Be CAREFULL with this
- # number. Setting it too high causes long processing time during the
- # application start-up.
- # If NEWS_TIME_PERIOD = 180, it takes approx. 35 minutes to calculate
- # annomalies for all tests.
- NEWS_TIME_PERIOD = 21 # [days]
-
- ############################################################################
- # Report.
-
- # The pathname prefix for the application.
- REPORT_ROUTES_PATHNAME_PREFIX = "/report/"
-
- # Path and name of the file specifying the HTML layout of the dash
- # application.
- REPORT_HTML_LAYOUT_FILE = "pal/templates/report_layout.jinja2"
-
- # Layout of plot.ly graphs.
- REPORT_GRAPH_LAYOUT_FILE = "pal/report/layout.yaml"
-
- # Default name of downloaded file with selected data.
- REPORT_DOWNLOAD_FILE_NAME = "iterative_data.csv"
-
- ############################################################################
- # Statistics.
-
- # The pathname prefix for the application.
- STATS_ROUTES_PATHNAME_PREFIX = "/stats/"
-
- # Path and name of the file specifying the HTML layout of the dash
- # application.
- STATS_HTML_LAYOUT_FILE = "pal/templates/stats_layout.jinja2"
-
- # Layout of plot.ly graphs.
- STATS_GRAPH_LAYOUT_FILE = "pal/stats/layout.yaml"
-
- # The default job displayed when the page is loaded first time.
- STATS_DEFAULT_JOB = "csit-vpp-perf-mrr-daily-master-2n-icx"
-
- # Default name of downloaded file with selected data.
- STATS_DOWNLOAD_FILE_NAME = "stats.csv"
-
- ############################################################################
- # Trending.
-
- # The pathname prefix for the application.
- TREND_ROUTES_PATHNAME_PREFIX = "/trending/"
-
- # Path and name of the file specifying the HTML layout of the dash
- # application.
- TREND_HTML_LAYOUT_FILE = "pal/templates/trending_layout.jinja2"
-
- # Layout of plot.ly graphs.
- TREND_GRAPH_LAYOUT_FILE = "pal/trending/layout.yaml"
-
- # Default name of downloaded file with selected data.
- TREND_DOWNLOAD_FILE_NAME = "trending_data.csv"
diff --git a/resources/tools/dash/app/pal/utils/tooltips.yaml b/resources/tools/dash/app/pal/utils/tooltips.yaml
deleted file mode 100644
index 2086b575a9..0000000000
--- a/resources/tools/dash/app/pal/utils/tooltips.yaml
+++ /dev/null
@@ -1,40 +0,0 @@
-help-area:
- The area defines a VPP packet path and lookup type.
-help-cadence:
- The cadence of the Jenkins job which runs the tests.
-help-cores:
- Number of cores the DUT uses during the test.
-help-download:
- Download the selected data as a csv file.
-help-dut:
- Device Under Test (DUT) - In software networking, “device” denotes a specific
- piece of software tasked with packet processing. Such device is surrounded
- with other software components (such as operating system kernel).
-help-dut-ver:
- The version of the Device under Test.
-help-framesize:
- Frame size - size of an Ethernet Layer-2 frame on the wire, including any VLAN
- tags (dot1q, dot1ad) and Ethernet FCS, but excluding Ethernet preamble and
- inter-frame gap. Measured in Bytes.
-help-infra:
- Infrastructure is defined by the toplology (number of nodes), processor
- architecture, NIC and driver.
-help-normalize:
- Normalize the results to CPU frequency 2GHz. The results from AWS environment
- are not normalized as we do not know the exact value of CPU frequency.
-help-release:
- The CSIT release.
-help-tbed:
- The test bed is defined by toplology (number of nodes) and processor
- architecture.
-help-test:
- The test specification consists of packet encapsulation, VPP packet processing
- (packet forwarding mode and packet processing function(s)) and packet
- forwarding path.
-help-time-period:
- Choose a time period for selected tests.
-help-ttype:
- Main measured variable.
-help-url:
- URL with current configuration. If there is no "Copy URL" button, use triple
- click.
diff --git a/resources/tools/dash/app/pal/utils/url_processing.py b/resources/tools/dash/app/pal/utils/url_processing.py
deleted file mode 100644
index 9307015d0d..0000000000
--- a/resources/tools/dash/app/pal/utils/url_processing.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""URL decoding and parsing and URL encoding.
-"""
-
-import logging
-
-from base64 import urlsafe_b64encode, urlsafe_b64decode
-from urllib.parse import urlencode, urlunparse, urlparse, parse_qs
-from zlib import compress, decompress
-from zlib import error as ZlibErr
-from binascii import Error as BinasciiErr
-
-
-def url_encode(params: dict) -> str:
- """Encode the URL parameters and zip them and create the whole URL using
- given data.
-
- :param params: All data necessary to create the URL:
- - scheme,
- - network location,
- - path,
- - query,
- - parameters.
- :type params: dict
- :returns: Encoded URL.
- :rtype: str
- """
-
- url_params = params.get("params", None)
- if url_params:
- encoded_params = urlsafe_b64encode(
- compress(urlencode(url_params).encode("utf-8"), level=9)
- ).rstrip(b"=").decode("utf-8")
- else:
- encoded_params = str()
-
- return urlunparse((
- params.get("scheme", "http"),
- params.get("netloc", str()),
- params.get("path", str()),
- str(), # params
- params.get("query", str()),
- encoded_params
- ))
-
-
-def url_decode(url: str) -> dict:
- """Parse the given URL and decode the parameters.
-
- :param url: URL to be parsed and decoded.
- :type url: str
- :returns: Paresed URL.
- :rtype: dict
- """
-
- try:
- parsed_url = urlparse(url)
- except ValueError as err:
- logging.warning(f"\nThe url {url} is not valid, ignoring.\n{repr(err)}")
- return None
-
- if parsed_url.fragment:
- try:
- padding = b"=" * (4 - (len(parsed_url.fragment) % 4))
- params = parse_qs(decompress(
- urlsafe_b64decode(
- (parsed_url.fragment.encode("utf-8") + padding)
- )).decode("utf-8")
- )
- except (BinasciiErr, UnicodeDecodeError, ZlibErr) as err:
- logging.warning(
- f"\nNot possible to decode the parameters from url: {url}"
- f"\nEncoded parameters: '{parsed_url.fragment}'"
- f"\n{repr(err)}"
- )
- return None
- else:
- params = None
-
- return {
- "scheme": parsed_url.scheme,
- "netloc": parsed_url.netloc,
- "path": parsed_url.path,
- "query": parsed_url.query,
- "fragment": parsed_url.fragment,
- "params": params
- }
diff --git a/resources/tools/dash/app/pal/utils/utils.py b/resources/tools/dash/app/pal/utils/utils.py
deleted file mode 100644
index 9e4eeeb892..0000000000
--- a/resources/tools/dash/app/pal/utils/utils.py
+++ /dev/null
@@ -1,344 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-"""Function used by Dash applications.
-"""
-
-import pandas as pd
-import dash_bootstrap_components as dbc
-
-from numpy import isnan
-from dash import dcc
-from datetime import datetime
-
-from ..jumpavg import classify
-from ..utils.constants import Constants as C
-from ..utils.url_processing import url_encode
-
-
-def classify_anomalies(data):
- """Process the data and return anomalies and trending values.
-
- Gather data into groups with average as trend value.
- Decorate values within groups to be normal,
- the first value of changed average as a regression, or a progression.
-
- :param data: Full data set with unavailable samples replaced by nan.
- :type data: OrderedDict
- :returns: Classification and trend values
- :rtype: 3-tuple, list of strings, list of floats and list of floats
- """
- # NaN means something went wrong.
- # Use 0.0 to cause that being reported as a severe regression.
- bare_data = [0.0 if isnan(sample) else sample for sample in data.values()]
- # TODO: Make BitCountingGroupList a subclass of list again?
- group_list = classify(bare_data).group_list
- group_list.reverse() # Just to use .pop() for FIFO.
- classification = list()
- avgs = list()
- stdevs = list()
- active_group = None
- values_left = 0
- avg = 0.0
- stdv = 0.0
- for sample in data.values():
- if isnan(sample):
- classification.append("outlier")
- avgs.append(sample)
- stdevs.append(sample)
- continue
- if values_left < 1 or active_group is None:
- values_left = 0
- while values_left < 1: # Ignore empty groups (should not happen).
- active_group = group_list.pop()
- values_left = len(active_group.run_list)
- avg = active_group.stats.avg
- stdv = active_group.stats.stdev
- classification.append(active_group.comment)
- avgs.append(avg)
- stdevs.append(stdv)
- values_left -= 1
- continue
- classification.append("normal")
- avgs.append(avg)
- stdevs.append(stdv)
- values_left -= 1
- return classification, avgs, stdevs
-
-
-def get_color(idx: int) -> str:
- """Returns a color from the list defined in Constants.PLOT_COLORS defined by
- its index.
-
- :param idx: Index of the color.
- :type idx: int
- :returns: Color defined by hex code.
- :trype: str
- """
- return C.PLOT_COLORS[idx % len(C.PLOT_COLORS)]
-
-
-def show_tooltip(tooltips:dict, id: str, title: str,
- clipboard_id: str=None) -> list:
- """Generate list of elements to display a text (e.g. a title) with a
- tooltip and optionaly with Copy&Paste icon and the clipboard
- functionality enabled.
-
- :param tooltips: Dictionary with tooltips.
- :param id: Tooltip ID.
- :param title: A text for which the tooltip will be displayed.
- :param clipboard_id: If defined, a Copy&Paste icon is displayed and the
- clipboard functionality is enabled.
- :type tooltips: dict
- :type id: str
- :type title: str
- :type clipboard_id: str
- :returns: List of elements to display a text with a tooltip and
- optionaly with Copy&Paste icon.
- :rtype: list
- """
-
- return [
- dcc.Clipboard(target_id=clipboard_id, title="Copy URL") \
- if clipboard_id else str(),
- f"{title} ",
- dbc.Badge(
- id=id,
- children="?",
- pill=True,
- color="white",
- text_color="info",
- class_name="border ms-1",
- ),
- dbc.Tooltip(
- children=tooltips.get(id, str()),
- target=id,
- placement="auto"
- )
- ]
-
-
-def label(key: str) -> str:
- """Returns a label for input elements (dropdowns, ...).
-
- If the label is not defined, the function returns the provided key.
-
- :param key: The key to the label defined in Constants.LABELS.
- :type key: str
- :returns: Label.
- :rtype: str
- """
- return C.LABELS.get(key, key)
-
-
-def sync_checklists(options: list, sel: list, all: list, id: str) -> tuple:
- """Synchronize a checklist with defined "options" with its "All" checklist.
-
- :param options: List of options for the cheklist.
- :param sel: List of selected options.
- :param all: List of selected option from "All" checklist.
- :param id: ID of a checklist to be used for synchronization.
- :returns: Tuple of lists with otions for both checklists.
- :rtype: tuple of lists
- """
- opts = {v["value"] for v in options}
- if id =="all":
- sel = list(opts) if all else list()
- else:
- all = ["all", ] if set(sel) == opts else list()
- return sel, all
-
-
-def list_tests(selection: dict) -> list:
- """Transform list of tests to a list of dictionaries usable by checkboxes.
-
- :param selection: List of tests to be displayed in "Selected tests" window.
- :type selection: list
- :returns: List of dictionaries with "label", "value" pairs for a checkbox.
- :rtype: list
- """
- if selection:
- return [{"label": v["id"], "value": v["id"]} for v in selection]
- else:
- return list()
-
-
-def get_date(s_date: str) -> datetime:
- """Transform string reprezentation of date to datetime.datetime data type.
-
- :param s_date: String reprezentation of date.
- :type s_date: str
- :returns: Date as datetime.datetime.
- :rtype: datetime.datetime
- """
- return datetime(int(s_date[0:4]), int(s_date[5:7]), int(s_date[8:10]))
-
-
-def gen_new_url(url_components: dict, params: dict) -> str:
- """Generate a new URL with encoded parameters.
-
- :param url_components: Dictionary with URL elements. It should contain
- "scheme", "netloc" and "path".
- :param url_components: URL parameters to be encoded to the URL.
- :type parsed_url: dict
- :type params: dict
- :returns Encoded URL with parameters.
- :rtype: str
- """
-
- if url_components:
- return url_encode(
- {
- "scheme": url_components.get("scheme", ""),
- "netloc": url_components.get("netloc", ""),
- "path": url_components.get("path", ""),
- "params": params
- }
- )
- else:
- return str()
-
-
-def get_duts(df: pd.DataFrame) -> list:
- """Get the list of DUTs from the pre-processed information about jobs.
-
- :param df: DataFrame with information about jobs.
- :type df: pandas.DataFrame
- :returns: Alphabeticaly sorted list of DUTs.
- :rtype: list
- """
- return sorted(list(df["dut"].unique()))
-
-
-def get_ttypes(df: pd.DataFrame, dut: str) -> list:
- """Get the list of test types from the pre-processed information about
- jobs.
-
- :param df: DataFrame with information about jobs.
- :param dut: The DUT for which the list of test types will be populated.
- :type df: pandas.DataFrame
- :type dut: str
- :returns: Alphabeticaly sorted list of test types.
- :rtype: list
- """
- return sorted(list(df.loc[(df["dut"] == dut)]["ttype"].unique()))
-
-
-def get_cadences(df: pd.DataFrame, dut: str, ttype: str) -> list:
- """Get the list of cadences from the pre-processed information about
- jobs.
-
- :param df: DataFrame with information about jobs.
- :param dut: The DUT for which the list of cadences will be populated.
- :param ttype: The test type for which the list of cadences will be
- populated.
- :type df: pandas.DataFrame
- :type dut: str
- :type ttype: str
- :returns: Alphabeticaly sorted list of cadences.
- :rtype: list
- """
- return sorted(list(df.loc[(
- (df["dut"] == dut) &
- (df["ttype"] == ttype)
- )]["cadence"].unique()))
-
-
-def get_test_beds(df: pd.DataFrame, dut: str, ttype: str, cadence: str) -> list:
- """Get the list of test beds from the pre-processed information about
- jobs.
-
- :param df: DataFrame with information about jobs.
- :param dut: The DUT for which the list of test beds will be populated.
- :param ttype: The test type for which the list of test beds will be
- populated.
- :param cadence: The cadence for which the list of test beds will be
- populated.
- :type df: pandas.DataFrame
- :type dut: str
- :type ttype: str
- :type cadence: str
- :returns: Alphabeticaly sorted list of test beds.
- :rtype: list
- """
- return sorted(list(df.loc[(
- (df["dut"] == dut) &
- (df["ttype"] == ttype) &
- (df["cadence"] == cadence)
- )]["tbed"].unique()))
-
-
-def get_job(df: pd.DataFrame, dut, ttype, cadence, testbed):
- """Get the name of a job defined by dut, ttype, cadence, test bed.
- Input information comes from the control panel.
-
- :param df: DataFrame with information about jobs.
- :param dut: The DUT for which the job name will be created.
- :param ttype: The test type for which the job name will be created.
- :param cadence: The cadence for which the job name will be created.
- :param testbed: The test bed for which the job name will be created.
- :type df: pandas.DataFrame
- :type dut: str
- :type ttype: str
- :type cadence: str
- :type testbed: str
- :returns: Job name.
- :rtype: str
- """
- return df.loc[(
- (df["dut"] == dut) &
- (df["ttype"] == ttype) &
- (df["cadence"] == cadence) &
- (df["tbed"] == testbed)
- )]["job"].item()
-
-
-def generate_options(opts: list) -> list:
- """Return list of options for radio items in control panel. The items in
- the list are dictionaries with keys "label" and "value".
-
- :params opts: List of options (str) to be used for the generated list.
- :type opts: list
- :returns: List of options (dict).
- :rtype: list
- """
- return [{"label": i, "value": i} for i in opts]
-
-
-def set_job_params(df: pd.DataFrame, job: str) -> dict:
- """Create a dictionary with all options and values for (and from) the
- given job.
-
- :param df: DataFrame with information about jobs.
- :params job: The name of job for and from which the dictionary will be
- created.
- :type df: pandas.DataFrame
- :type job: str
- :returns: Dictionary with all options and values for (and from) the
- given job.
- :rtype: dict
- """
-
- l_job = job.split("-")
- return {
- "job": job,
- "dut": l_job[1],
- "ttype": l_job[3],
- "cadence": l_job[4],
- "tbed": "-".join(l_job[-2:]),
- "duts": generate_options(get_duts(df)),
- "ttypes": generate_options(get_ttypes(df, l_job[1])),
- "cadences": generate_options(get_cadences(df, l_job[1], l_job[3])),
- "tbeds": generate_options(
- get_test_beds(df, l_job[1], l_job[3], l_job[4]))
- }
diff --git a/resources/tools/dash/app/requirements.txt b/resources/tools/dash/app/requirements.txt
deleted file mode 100644
index d09eecd2d9..0000000000
--- a/resources/tools/dash/app/requirements.txt
+++ /dev/null
@@ -1,39 +0,0 @@
-attrs==21.2.0
-awswrangler==2.14.0
-Brotli==1.0.9
-click==8.0.3
-dash==2.0.0
-dash-core-components==2.0.0
-dash_bootstrap_components==1.1.0
-dash-html-components==2.0.0
-dash-renderer==1.9.1
-dash-table==5.0.0
-Flask==2.0.2
-Flask-Assets==2.0
-Flask-Compress==1.10.1
-hdrhistogram==0.9.1
-future==0.18.2
-intervaltree==3.1.0
-itsdangerous==2.0.1
-Jinja2==3.0.3
-MarkupSafe==2.0.1
-numpy==1.21.4
-packaging==21.3
-pandas==1.3.5
-pip==21.2.4
-plotly==5.4.0
-protobuf==3.19.1
-pyparsing==3.0.6
-python-dateutil==2.8.2
-python-dotenv==0.19.2
-pytz==2021.3
-PyYAML==5.1
-retrying==1.3.3
-setuptools==57.5.0
-six==1.16.0
-sortedcontainers==2.4.0
-tenacity==8.0.1
-uWSGI==2.0.20
-webassets==2.0
-Werkzeug==2.0.2
-wheel==0.37.0 \ No newline at end of file
diff --git a/resources/tools/dash/app/wsgi.py b/resources/tools/dash/app/wsgi.py
deleted file mode 100644
index ab18bbfbc7..0000000000
--- a/resources/tools/dash/app/wsgi.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright (c) 2022 Cisco and/or its affiliates.
-# 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:
-#
-# http://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.
-
-
-from pal import app
-
-
-if __name__ == "__main__":
- # Main entry point.
- app.debug = True
- app.run(host="0.0.0.0")