diff options
Diffstat (limited to 'resources/tools/dash/app')
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") |