feat(uti): Add iterative data 55/36155/8
authorTibor Frank <tifrank@cisco.com>
Tue, 17 May 2022 12:30:37 +0000 (14:30 +0200)
committerTibor Frank <tifrank@cisco.com>
Thu, 19 May 2022 11:58:43 +0000 (11:58 +0000)
Change-Id: Iaa7253b377f019235289f6bbf48eafd850a2dfc8
Signed-off-by: Tibor Frank <tifrank@cisco.com>
14 files changed:
resources/tools/dash/app/pal/__init__.py
resources/tools/dash/app/pal/data/data.py
resources/tools/dash/app/pal/data/data.yaml
resources/tools/dash/app/pal/data/tooltips.yaml
resources/tools/dash/app/pal/report/data.py [deleted file]
resources/tools/dash/app/pal/report/graphs.py [new file with mode: 0644]
resources/tools/dash/app/pal/report/layout.py
resources/tools/dash/app/pal/report/layout.yaml [new file with mode: 0644]
resources/tools/dash/app/pal/report/report.py
resources/tools/dash/app/pal/stats/layout.py
resources/tools/dash/app/pal/stats/stats.py
resources/tools/dash/app/pal/templates/report_layout.jinja2 [new file with mode: 0644]
resources/tools/dash/app/pal/trending/layout.py
resources/tools/dash/app/pal/trending/trending.py

index 4e32598..bb110a2 100644 (file)
@@ -30,6 +30,10 @@ MAX_TIME_PERIOD = 180
 # 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=["rls2202", ]
 
 def init_app():
     """Construct core Flask application with embedded Dash app.
@@ -65,6 +69,9 @@ def init_app():
         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=RELEASES)
+
     return app
 
 
index 3d9b8b1..efe2a2d 100644 (file)
 
 """Prepare data for Plotly Dash."""
 
-from datetime import datetime, timedelta
 import logging
+
+from yaml import load, FullLoader, YAMLError
+from datetime import datetime, timedelta
 from time import time
+from pytz import UTC
+from pandas import DataFrame
 
 import awswrangler as wr
-from pytz import UTC
 
-from yaml import load, FullLoader, YAMLError
 from awswrangler.exceptions import EmptyDataFrame, NoFilesFound
 
 
@@ -28,7 +30,7 @@ class Data:
     """
     """
 
-    def __init__(self, data_spec_file, debug=False):
+    def __init__(self, data_spec_file: str, debug: bool=False) -> None:
         """
         """
 
@@ -61,7 +63,7 @@ class Data:
     def data(self):
         return self._data
 
-    def _get_columns(self, parquet):
+    def _get_columns(self, parquet: str) -> list:
         try:
             return self._data_spec[parquet]["columns"]
         except KeyError as err:
@@ -71,7 +73,7 @@ class Data:
                 f"specified.\n{err}"
             )
 
-    def _get_path(self, parquet):
+    def _get_path(self, parquet: str) -> str:
         try:
             return self._data_spec[parquet]["path"]
         except KeyError as err:
@@ -84,7 +86,7 @@ class Data:
     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):
+        last_modified_end=None, days=None) -> DataFrame:
         """Read parquet stored in S3 compatible storage and returns Pandas
         Dataframe.
 
@@ -148,7 +150,7 @@ class Data:
         self._data = df
         return df
 
-    def read_stats(self, days=None):
+    def read_stats(self, days: int=None) -> tuple:
         """Read Suite Result Analysis data partition from parquet.
         """
         l_stats = lambda part: True if part["stats_type"] == "sra" else False
@@ -176,7 +178,7 @@ class Data:
             )
         )
 
-    def read_trending_mrr(self, days=None):
+    def read_trending_mrr(self, days: int=None) -> DataFrame:
         """Read MRR data partition from parquet.
         """
         lambda_f = lambda part: True if part["test_type"] == "mrr" else False
@@ -188,7 +190,7 @@ class Data:
             days=days
         )
 
-    def read_trending_ndrpdr(self, days=None):
+    def read_trending_ndrpdr(self, days: int=None) -> DataFrame:
         """Read NDRPDR data partition from iterative parquet.
         """
         lambda_f = lambda part: True if part["test_type"] == "ndrpdr" else False
@@ -200,26 +202,24 @@ class Data:
             days=days
         )
 
-    def read_iterative_mrr(self, days=None):
+    def read_iterative_mrr(self, release: str) -> DataFrame:
         """Read MRR data partition from iterative parquet.
         """
         lambda_f = lambda part: True if part["test_type"] == "mrr" else False
 
         return self._create_dataframe_from_parquet(
-            path=self._get_path("iterative-mrr"),
+            path=self._get_path("iterative-mrr").format(release=release),
             partition_filter=lambda_f,
-            columns=self._get_columns("iterative-mrr"),
-            days=days
+            columns=self._get_columns("iterative-mrr")
         )
 
-    def read_iterative_ndrpdr(self, days=None):
+    def read_iterative_ndrpdr(self, release: str) -> DataFrame:
         """Read NDRPDR data partition from parquet.
         """
         lambda_f = lambda part: True if part["test_type"] == "ndrpdr" else False
 
         return self._create_dataframe_from_parquet(
-            path=self._get_path("iterative-ndrpdr"),
+            path=self._get_path("iterative-ndrpdr").format(release=release),
             partition_filter=lambda_f,
-            columns=self._get_columns("iterative-ndrpdr"),
-            days=days
+            columns=self._get_columns("iterative-ndrpdr")
         )
index 92cd659..15fad71 100644 (file)
@@ -104,7 +104,7 @@ trending-ndrpdr:
     # - result_latency_forward_pdr_0_min
     # - result_latency_forward_pdr_0_unit
 iterative-mrr:
-  path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/iterative_rls2202
+  path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/iterative_{release}
   columns:
     - job
     - build
@@ -122,7 +122,7 @@ iterative-mrr:
     - result_receive_rate_rate_unit
     - result_receive_rate_rate_values
 iterative-ndrpdr:
-  path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/iterative_rls2202
+  path: s3://fdio-docs-s3-cloudfront-index/csit/parquet/iterative_{release}
   columns:
     - job
     - build
@@ -135,62 +135,62 @@ iterative-ndrpdr:
     - test_name_long
     - test_name_short
     - version
-    - result_pdr_upper_rate_unit
-    - result_pdr_upper_rate_value
-    - result_pdr_upper_bandwidth_unit
-    - result_pdr_upper_bandwidth_value
+    - result_pdr_upper_rate_unit
+    - result_pdr_upper_rate_value
+    - result_pdr_upper_bandwidth_unit
+    - result_pdr_upper_bandwidth_value
     - result_pdr_lower_rate_unit
     - result_pdr_lower_rate_value
-    - result_pdr_lower_bandwidth_unit
-    - result_pdr_lower_bandwidth_value
-    - result_ndr_upper_rate_unit
-    - result_ndr_upper_rate_value
-    - result_ndr_upper_bandwidth_unit
-    - result_ndr_upper_bandwidth_value
+    - result_pdr_lower_bandwidth_unit
+    - result_pdr_lower_bandwidth_value
+    - result_ndr_upper_rate_unit
+    - result_ndr_upper_rate_value
+    - result_ndr_upper_bandwidth_unit
+    - result_ndr_upper_bandwidth_value
     - result_ndr_lower_rate_unit
     - result_ndr_lower_rate_value
-    - result_ndr_lower_bandwidth_unit
-    - result_ndr_lower_bandwidth_value
-    - result_latency_reverse_pdr_90_avg
+    - result_ndr_lower_bandwidth_unit
+    - result_ndr_lower_bandwidth_value
+    - result_latency_reverse_pdr_90_avg
     - result_latency_reverse_pdr_90_hdrh
-    - result_latency_reverse_pdr_90_max
-    - result_latency_reverse_pdr_90_min
-    - result_latency_reverse_pdr_90_unit
-    - result_latency_reverse_pdr_50_avg
+    - result_latency_reverse_pdr_90_max
+    - result_latency_reverse_pdr_90_min
+    - result_latency_reverse_pdr_90_unit
+    - result_latency_reverse_pdr_50_avg
     - result_latency_reverse_pdr_50_hdrh
-    - result_latency_reverse_pdr_50_max
-    - result_latency_reverse_pdr_50_min
-    - result_latency_reverse_pdr_50_unit
-    - result_latency_reverse_pdr_10_avg
+    - result_latency_reverse_pdr_50_max
+    - result_latency_reverse_pdr_50_min
+    - result_latency_reverse_pdr_50_unit
+    - result_latency_reverse_pdr_10_avg
     - result_latency_reverse_pdr_10_hdrh
-    - result_latency_reverse_pdr_10_max
-    - result_latency_reverse_pdr_10_min
-    - result_latency_reverse_pdr_10_unit
-    - result_latency_reverse_pdr_0_avg
+    - result_latency_reverse_pdr_10_max
+    - result_latency_reverse_pdr_10_min
+    - result_latency_reverse_pdr_10_unit
+    - result_latency_reverse_pdr_0_avg
     - result_latency_reverse_pdr_0_hdrh
-    - result_latency_reverse_pdr_0_max
-    - result_latency_reverse_pdr_0_min
-    - result_latency_reverse_pdr_0_unit
-    - result_latency_forward_pdr_90_avg
+    - result_latency_reverse_pdr_0_max
+    - result_latency_reverse_pdr_0_min
+    - result_latency_reverse_pdr_0_unit
+    - result_latency_forward_pdr_90_avg
     - result_latency_forward_pdr_90_hdrh
-    - result_latency_forward_pdr_90_max
-    - result_latency_forward_pdr_90_min
-    - result_latency_forward_pdr_90_unit
+    - result_latency_forward_pdr_90_max
+    - result_latency_forward_pdr_90_min
+    - result_latency_forward_pdr_90_unit
     - result_latency_forward_pdr_50_avg
     - result_latency_forward_pdr_50_hdrh
-    - result_latency_forward_pdr_50_max
-    - result_latency_forward_pdr_50_min
+    - result_latency_forward_pdr_50_max
+    - result_latency_forward_pdr_50_min
     - result_latency_forward_pdr_50_unit
-    - result_latency_forward_pdr_10_avg
+    - result_latency_forward_pdr_10_avg
     - result_latency_forward_pdr_10_hdrh
-    - result_latency_forward_pdr_10_max
-    - result_latency_forward_pdr_10_min
-    - result_latency_forward_pdr_10_unit
-    - result_latency_forward_pdr_0_avg
+    - result_latency_forward_pdr_10_max
+    - result_latency_forward_pdr_10_min
+    - result_latency_forward_pdr_10_unit
+    - result_latency_forward_pdr_0_avg
     - result_latency_forward_pdr_0_hdrh
-    - result_latency_forward_pdr_0_max
-    - result_latency_forward_pdr_0_min
-    - result_latency_forward_pdr_0_unit
+    - result_latency_forward_pdr_0_max
+    - result_latency_forward_pdr_0_min
+    - result_latency_forward_pdr_0_unit
 # coverage-ndrpdr:
 #   path: str
 #   columns:
index e5259b4..5a830e4 100644 (file)
@@ -10,6 +10,8 @@ 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
@@ -17,6 +19,8 @@ help-framesize:
 help-infra:
   Infrastructure is defined by the toplology (number of nodes), processor
   architecture, NIC and driver.
+help-release:
+  The CSIT release.
 help-tbed:
   The test bed is defined by toplology (number of nodes) and processor
   architecture.
diff --git a/resources/tools/dash/app/pal/report/data.py b/resources/tools/dash/app/pal/report/data.py
deleted file mode 100644 (file)
index 848259b..0000000
+++ /dev/null
@@ -1,268 +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."""
-
-from logging import info
-from time import time
-
-import awswrangler as wr
-from awswrangler.exceptions import EmptyDataFrame, NoFilesFound
-from boto3 import session
-
-
-S3_DOCS_BUCKET="fdio-docs-s3-cloudfront-index"
-
-def create_dataframe_from_parquet(
-        path, partition_filter=None, columns=None,
-        validate_schema=False, last_modified_begin=None,
-        last_modified_end=None):
-    """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.
-    :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
-    :returns: Pandas DataFrame or None if DataFrame cannot be fetched.
-    :rtype: DataFrame
-    """
-    df = None
-    start = time()
-    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
-        )
-        info(f"Create dataframe {path} took: {time() - start}")
-        info(df)
-        info(df.info(memory_usage="deep"))
-    except NoFilesFound:
-        return df
-
-    return df
-
-
-def read_stats():
-    """Read Suite Result Analysis data partition from parquet.
-    """
-    lambda_f = lambda part: True if part["stats_type"] == "sra" else False
-
-    return create_dataframe_from_parquet(
-        path=f"s3://{S3_DOCS_BUCKET}/csit/parquet/stats",
-        partition_filter=lambda_f
-    )
-
-def read_trending_mrr():
-    """Read MRR data partition from parquet.
-    """
-    lambda_f = lambda part: True if part["test_type"] == "mrr" else False
-
-    return create_dataframe_from_parquet(
-        path=f"s3://{S3_DOCS_BUCKET}/csit/parquet/trending",
-        partition_filter=lambda_f,
-        columns=["job", "build", "dut_type", "dut_version", "hosts",
-            "start_time", "passed", "test_id", "test_name_long",
-            "test_name_short", "version",
-            "result_receive_rate_rate_avg",
-            "result_receive_rate_rate_stdev",
-            "result_receive_rate_rate_unit",
-            "result_receive_rate_rate_values"
-        ]
-    )
-
-def read_iterative_mrr():
-    """Read MRR data partition from iterative parquet.
-    """
-    lambda_f = lambda part: True if part["test_type"] == "mrr" else False
-
-    return create_dataframe_from_parquet(
-        path=f"s3://{S3_DOCS_BUCKET}/csit/parquet/iterative_rls2202",
-        partition_filter=lambda_f,
-        columns=["job", "build", "dut_type", "dut_version", "hosts",
-            "start_time", "passed", "test_id", "test_name_long",
-            "test_name_short", "version",
-            "result_receive_rate_rate_avg",
-            "result_receive_rate_rate_stdev",
-            "result_receive_rate_rate_unit",
-            "result_receive_rate_rate_values"
-        ]
-    )
-
-def read_trending_ndrpdr():
-    """Read NDRPDR data partition from iterative parquet.
-    """
-    lambda_f = lambda part: True if part["test_type"] == "ndrpdr" else False
-
-    return create_dataframe_from_parquet(
-        path=f"s3://{S3_DOCS_BUCKET}/csit/parquet/trending",
-        partition_filter=lambda_f,
-        columns=["job", "build", "dut_type", "dut_version", "hosts",
-            "start_time", "passed", "test_id", "test_name_long",
-            "test_name_short", "version",
-            "result_pdr_upper_rate_unit",
-            "result_pdr_upper_rate_value",
-            "result_pdr_upper_bandwidth_unit",
-            "result_pdr_upper_bandwidth_value",
-            "result_pdr_lower_rate_unit",
-            "result_pdr_lower_rate_value",
-            "result_pdr_lower_bandwidth_unit",
-            "result_pdr_lower_bandwidth_value",
-            "result_ndr_upper_rate_unit",
-            "result_ndr_upper_rate_value",
-            "result_ndr_upper_bandwidth_unit",
-            "result_ndr_upper_bandwidth_value",
-            "result_ndr_lower_rate_unit",
-            "result_ndr_lower_rate_value",
-            "result_ndr_lower_bandwidth_unit",
-            "result_ndr_lower_bandwidth_value",
-            "result_latency_reverse_pdr_90_avg",
-            "result_latency_reverse_pdr_90_hdrh",
-            "result_latency_reverse_pdr_90_max",
-            "result_latency_reverse_pdr_90_min",
-            "result_latency_reverse_pdr_90_unit",
-            "result_latency_reverse_pdr_50_avg",
-            "result_latency_reverse_pdr_50_hdrh",
-            "result_latency_reverse_pdr_50_max",
-            "result_latency_reverse_pdr_50_min",
-            "result_latency_reverse_pdr_50_unit",
-            "result_latency_reverse_pdr_10_avg",
-            "result_latency_reverse_pdr_10_hdrh",
-            "result_latency_reverse_pdr_10_max",
-            "result_latency_reverse_pdr_10_min",
-            "result_latency_reverse_pdr_10_unit",
-            "result_latency_reverse_pdr_0_avg",
-            "result_latency_reverse_pdr_0_hdrh",
-            "result_latency_reverse_pdr_0_max",
-            "result_latency_reverse_pdr_0_min",
-            "result_latency_reverse_pdr_0_unit",
-            "result_latency_forward_pdr_90_avg",
-            "result_latency_forward_pdr_90_hdrh",
-            "result_latency_forward_pdr_90_max",
-            "result_latency_forward_pdr_90_min",
-            "result_latency_forward_pdr_90_unit",
-            "result_latency_forward_pdr_50_avg",
-            "result_latency_forward_pdr_50_hdrh",
-            "result_latency_forward_pdr_50_max",
-            "result_latency_forward_pdr_50_min",
-            "result_latency_forward_pdr_50_unit",
-            "result_latency_forward_pdr_10_avg",
-            "result_latency_forward_pdr_10_hdrh",
-            "result_latency_forward_pdr_10_max",
-            "result_latency_forward_pdr_10_min",
-            "result_latency_forward_pdr_10_unit",
-            "result_latency_forward_pdr_0_avg",
-            "result_latency_forward_pdr_0_hdrh",
-            "result_latency_forward_pdr_0_max",
-            "result_latency_forward_pdr_0_min",
-            "result_latency_forward_pdr_0_unit"
-        ]
-    )
-
-def read_iterative_ndrpdr():
-    """Read NDRPDR data partition from parquet.
-    """
-    lambda_f = lambda part: True if part["test_type"] == "ndrpdr" else False
-
-    return create_dataframe_from_parquet(
-        path=f"s3://{S3_DOCS_BUCKET}/csit/parquet/iterative_rls2202",
-        partition_filter=lambda_f,
-        columns=["job", "build", "dut_type", "dut_version", "hosts",
-            "start_time", "passed", "test_id", "test_name_long",
-            "test_name_short", "version",
-            "result_pdr_upper_rate_unit",
-            "result_pdr_upper_rate_value",
-            "result_pdr_upper_bandwidth_unit",
-            "result_pdr_upper_bandwidth_value",
-            "result_pdr_lower_rate_unit",
-            "result_pdr_lower_rate_value",
-            "result_pdr_lower_bandwidth_unit",
-            "result_pdr_lower_bandwidth_value",
-            "result_ndr_upper_rate_unit",
-            "result_ndr_upper_rate_value",
-            "result_ndr_upper_bandwidth_unit",
-            "result_ndr_upper_bandwidth_value",
-            "result_ndr_lower_rate_unit",
-            "result_ndr_lower_rate_value",
-            "result_ndr_lower_bandwidth_unit",
-            "result_ndr_lower_bandwidth_value",
-            "result_latency_reverse_pdr_90_avg",
-            "result_latency_reverse_pdr_90_hdrh",
-            "result_latency_reverse_pdr_90_max",
-            "result_latency_reverse_pdr_90_min",
-            "result_latency_reverse_pdr_90_unit",
-            "result_latency_reverse_pdr_50_avg",
-            "result_latency_reverse_pdr_50_hdrh",
-            "result_latency_reverse_pdr_50_max",
-            "result_latency_reverse_pdr_50_min",
-            "result_latency_reverse_pdr_50_unit",
-            "result_latency_reverse_pdr_10_avg",
-            "result_latency_reverse_pdr_10_hdrh",
-            "result_latency_reverse_pdr_10_max",
-            "result_latency_reverse_pdr_10_min",
-            "result_latency_reverse_pdr_10_unit",
-            "result_latency_reverse_pdr_0_avg",
-            "result_latency_reverse_pdr_0_hdrh",
-            "result_latency_reverse_pdr_0_max",
-            "result_latency_reverse_pdr_0_min",
-            "result_latency_reverse_pdr_0_unit",
-            "result_latency_forward_pdr_90_avg",
-            "result_latency_forward_pdr_90_hdrh",
-            "result_latency_forward_pdr_90_max",
-            "result_latency_forward_pdr_90_min",
-            "result_latency_forward_pdr_90_unit",
-            "result_latency_forward_pdr_50_avg",
-            "result_latency_forward_pdr_50_hdrh",
-            "result_latency_forward_pdr_50_max",
-            "result_latency_forward_pdr_50_min",
-            "result_latency_forward_pdr_50_unit",
-            "result_latency_forward_pdr_10_avg",
-            "result_latency_forward_pdr_10_hdrh",
-            "result_latency_forward_pdr_10_max",
-            "result_latency_forward_pdr_10_min",
-            "result_latency_forward_pdr_10_unit",
-            "result_latency_forward_pdr_0_avg",
-            "result_latency_forward_pdr_0_hdrh",
-            "result_latency_forward_pdr_0_max",
-            "result_latency_forward_pdr_0_min",
-            "result_latency_forward_pdr_0_unit"
-        ]
-    )
diff --git a/resources/tools/dash/app/pal/report/graphs.py b/resources/tools/dash/app/pal/report/graphs.py
new file mode 100644 (file)
index 0000000..751eb34
--- /dev/null
@@ -0,0 +1,224 @@
+# 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
+
+
+_COLORS = (
+    u"#1A1110", u"#DA2647", u"#214FC6", u"#01786F", u"#BD8260", u"#FFD12A",
+    u"#A6E7FF", u"#738276", u"#C95A49", u"#FC5A8D", u"#CEC8EF", u"#391285",
+    u"#6F2DA8", u"#FF878D", u"#45A27D", u"#FFD0B9", u"#FD5240", u"#DB91EF",
+    u"#44D7A8", u"#4F86F7", u"#84DE02", u"#FFCFF1", u"#614051"
+)
+_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"
+}
+_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"
+}
+_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 = {
+    u"result_latency_forward_pdr_0_hdrh": u"No-load.",
+    u"result_latency_reverse_pdr_0_hdrh": u"No-load.",
+    u"result_latency_forward_pdr_10_hdrh": u"Low-load, 10% PDR.",
+    u"result_latency_reverse_pdr_10_hdrh": u"Low-load, 10% PDR.",
+    u"result_latency_forward_pdr_50_hdrh": u"Mid-load, 50% PDR.",
+    u"result_latency_reverse_pdr_50_hdrh": u"Mid-load, 50% PDR.",
+    u"result_latency_forward_pdr_90_hdrh": u"High-load, 90% PDR.",
+    u"result_latency_reverse_pdr_90_hdrh": u"High-load, 90% PDR."
+}
+
+
+def select_iterative_data(data: pd.DataFrame, itm:dict) -> pd.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 = "none" if itm["dut"] == "trex" else itm["dut"].upper()
+
+    df = data.loc[(
+        (data["dut_type"] == dut) &
+        (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 graph_iterative(data: pd.DataFrame, sel:dict, layout: dict) -> tuple:
+    """
+    """
+
+    fig_tput = go.Figure()
+    fig_tsa = go.Figure()
+
+    return fig_tput, fig_tsa
+
+
+def table_comparison(data: pd.DataFrame, sel:dict) -> pd.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
+
+
+def graph_hdrh_latency(data: dict, layout: dict) -> go.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, PERCENTILE_MAX)
+            xaxis.append(previous_x)
+            yaxis.append(item.value_iterated_to)
+            hovertext.append(
+                f"<b>{_GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
+                f"Direction: {(u'W-E', u'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>{_GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
+                f"Direction: {(u'W-E', u'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=_GRAPH_LAT_HDRH_DESC[lat_name],
+                mode=u"lines",
+                legendgroup=_GRAPH_LAT_HDRH_DESC[lat_name],
+                showlegend=bool(idx % 2),
+                line=dict(
+                    color=_COLORS[int(idx/2)],
+                    dash=u"solid",
+                    width=1 if idx % 2 else 2
+                ),
+                hovertext=hovertext,
+                hoverinfo=u"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
index 70fe727..26b9a9f 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Plotly Dash HTML layout override."""
-
-html_layout = u"""
-<!DOCTYPE html>
-    <html>
-        <head>
-            {%metas%}
-            <title>{%title%}</title>
-            {%favicon%}
-            {%css%}
-        </head>
-        <body class="dash-template">
-            <header>
-              <div class="nav-wrapper">
-                <a href="/">
-                    <h1>FD.io CSIT</h1>
-                </a>
-                <a href="">
-                  <h1>Report</h1>
-                </a>
-                <nav>
-                </nav>
-              </div>
-            </header>
-            {%app_entry%}
-            <footer>
-                {%config%}
-                {%scripts%}
-                {%renderer%}
-            </footer>
-        </body>
-    </html>
+"""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 json import loads, JSONDecodeError
+from ast import literal_eval
+
+from pprint import pformat
+
+from ..data.data import Data
+from ..data.url_processing import url_decode, url_encode
+from .graphs import graph_iterative, table_comparison
+
+
+class Layout:
+    """
+    """
+
+    # If True, clear all inputs in control panel when button "ADD SELECTED" is
+    # pressed.
+    CLEAR_ALL_INPUTS = False
+
+    STYLE_DISABLED = {"display": "none"}
+    STYLE_ENABLED = {"display": "inherit"}
+
+    CL_ALL_DISABLED = [{
+        "label": "All",
+        "value": "all",
+        "disabled": True
+    }]
+    CL_ALL_ENABLED = [{
+        "label": "All",
+        "value": "all",
+        "disabled": False
+    }]
+
+    PLACEHOLDER = html.Nobr("")
+
+    DRIVERS = ("avf", "af-xdp", "rdma", "dpdk")
+
+    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 = {
+        "background-color": "#d2ebf5",
+        "border-color": "#bce1f1",
+        "color": "#135d7c"
+    }
+
+    def __init__(self, app: Flask, releases: list, html_layout_file: str,
+        graph_layout_file: str, data_spec_file: str, tooltip_file: str) -> None:
+        """
+        """
+
+        # 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)
+            data_mrr["release"] = rls
+            data_ndrpdr = Data(self._data_spec_file, True).\
+                read_iterative_ndrpdr(release=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"]
+            d_ver = row["dut_version"]
+            lst_job = row["job"].split("-")
+            dut = lst_job[1]
+            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 self.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(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_test = tbs[rls][dut][d_ver][infra][area][test]
+                tbs_test["core"] = list()
+                tbs_test["frame-size"] = list()
+                tbs_test["test-type"] = list()
+            if core.upper() not in tbs_test["core"]:
+                tbs_test["core"].append(core.upper())
+            if framesize.upper() not in tbs_test["frame-size"]:
+                tbs_test["frame-size"].append(framesize.upper())
+            if ttype == "mrr":
+                if "MRR" not in tbs_test["test-type"]:
+                    tbs_test["test-type"].append("MRR")
+            elif ttype == "ndrpdr":
+                if "NDR" not in tbs_test["test-type"]:
+                    tbs_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 label(self, key: str) -> str:
+        return self.LABELS.get(key, key)
+
+    def _show_tooltip(self, id: str, title: str,
+            clipboard_id: str=None) -> 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=self._tooltips.get(id, str()),
+                target=id,
+                placement="auto"
+            )
+        ]
+
+    def add_content(self):
+        """
+        """
+        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.
+        """
+        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.
+        """
+        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.
+        """
+        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=[
+                                            self.PLACEHOLDER
+                                        ]
+                                    ),
+                                    width=6
+                                ),
+                                dbc.Col(
+                                    dbc.Row(  # TSA
+                                        id="row-graph-tsa",
+                                        class_name="g-0 p-2",
+                                        children=[
+                                            self.PLACEHOLDER
+                                        ]
+                                    ),
+                                    width=6
+                                )
+                            ]
+                        ),
+                        dbc.Row(  # Tables
+                            id="row-table",
+                            class_name="g-0 p-2",
+                            children=[
+                                self.PLACEHOLDER
+                            ]
+                        ),
+                        dbc.Row(  # Download
+                            id="row-btn-download",
+                            class_name="g-0 p-2",
+                            children=[
+                                self.PLACEHOLDER
+                            ]
+                        )
+                    ]
+                )
+            ],
+            width=9
+        )
+
+    def _add_ctrl_panel(self) -> 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=self._show_tooltip(
+                                        "help-release", "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=self._show_tooltip(
+                                        "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=self._show_tooltip(
+                                        "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=self._show_tooltip(
+                                        "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=self._show_tooltip(
+                                        "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=self._show_tooltip(
+                                        "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=self._show_tooltip(
+                                "help-framesize", "Frame Size"),
+                            class_name="p-0"
+                        ),
+                        dbc.Col(
+                            children=[
+                                dbc.Checklist(
+                                    id="cl-ctrl-framesize-all",
+                                    options=self.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=self._show_tooltip(
+                                "help-cores", "Number of Cores"),
+                            class_name="p-0"
+                        ),
+                        dbc.Col(
+                            children=[
+                                dbc.Checklist(
+                                    id="cl-ctrl-core-all",
+                                    options=self.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=self._show_tooltip(
+                                "help-ttype", "Test Type"),
+                            class_name="p-0"
+                        ),
+                        dbc.Col(
+                            children=[
+                                dbc.Checklist(
+                                    id="cl-ctrl-testtype-all",
+                                    options=self.CL_ALL_DISABLED,
+                                    inline=True,
+                                    switch=False
+                                ),
+                            ],
+                            width=3
+                        ),
+                        dbc.Col(
+                            children=[
+                                dbc.Checklist(
+                                    id="cl-ctrl-testtype",
+                                    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=self.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=self.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:
+        def __init__(self, panel: dict) -> None:
+
+            CL_ALL_DISABLED = [{
+                "label": "All",
+                "value": "all",
+                "disabled": True
+            }]
+
+            # 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": CL_ALL_DISABLED,
+                "cl-framesize-options": list(),
+                "cl-framesize-value": list(),
+                "cl-framesize-all-value": list(),
+                "cl-framesize-all-options": CL_ALL_DISABLED,
+                "cl-testtype-options": list(),
+                "cl-testtype-value": list(),
+                "cl-testtype-all-value": list(),
+                "cl-testtype-all-options": CL_ALL_DISABLED,
+                "btn-add-disabled": True,
+                "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:
+            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:
+            return self._panel[key]
+
+        def values(self) -> tuple:
+            return tuple(self._panel.values())
+
+    @staticmethod
+    def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
+        """
+        """
+        options = {v["value"] for v in opt}
+        if id =="all":
+            sel = list(options) if all else list()
+        else:
+            all = ["all", ] if set(sel) == options else list()
+        return sel, all
+
+    @staticmethod
+    def _list_tests(selection: dict) -> list:
+        """Display selected tests with checkboxes
+        """
+        if selection:
+            return [{"label": v["id"], "value": v["id"]} for v in selection]
+        else:
+            return list()
+
+    def callbacks(self, app):
+
+        def _generate_plotting_area(figs: tuple, table: pd.DataFrame,
+                url: str) -> tuple:
+            """
+            """
+
+            (fig_tput, fig_tsa) = figs
+
+            row_fig_tput = self.PLACEHOLDER
+            row_fig_tsa = self.PLACEHOLDER
+            row_table = self.PLACEHOLDER
+            row_btn_dwnld = self.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=self._show_tooltip(
+                                        "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=self.URL_STYLE,
+                                        children=self._show_tooltip(
+                                            "help-url", "URL", "input-url")
+                                    ),
+                                    dbc.Input(
+                                        id="input-url",
+                                        readonly=True,
+                                        type="url",
+                                        style=self.URL_STYLE,
+                                        value=url
+                                    )
+                                ]
+                            )
+                        ]
+                    )
+                ]
+            if fig_tsa:
+                row_fig_tsa = [
+                    dcc.Graph(
+                        id={"type": "graph", "index": "lat"},
+                        figure=fig_tsa
+                    )
+                ]
+            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_tsa, 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-tsa", "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-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("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,
+            btn_add: int, btn_remove: int, btn_remove_all: int,
+            href: str) -> tuple:
+            """
+            """
+
+            def _gen_new_url(parsed_url: dict, store_sel: list) -> str:
+
+                if parsed_url:
+                    new_url = url_encode({
+                        "scheme": parsed_url["scheme"],
+                        "netloc": parsed_url["netloc"],
+                        "path": parsed_url["path"],
+                        "params": {
+                            "store_sel": store_sel,
+                        }
+                    })
+                else:
+                    new_url = str()
+                return new_url
+
+
+            ctrl_panel = self.ControlPanel(cp_data)
+
+            # Parse the url:
+            parsed_url = url_decode(href)
+
+            row_fig_tput = no_update
+            row_fig_tsa = 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": self.CL_ALL_DISABLED,
+                    "cl-framesize-options": list(),
+                    "cl-framesize-value": list(),
+                    "cl-framesize-all-value": list(),
+                    "cl-framesize-all-options": self.CL_ALL_DISABLED,
+                    "cl-testtype-options": list(),
+                    "cl-testtype-value": list(),
+                    "cl-testtype-all-value": list(),
+                    "cl-testtype-all-options": self.CL_ALL_DISABLED
+                })
+            if 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": self.CL_ALL_DISABLED,
+                    "cl-framesize-options": list(),
+                    "cl-framesize-value": list(),
+                    "cl-framesize-all-value": list(),
+                    "cl-framesize-all-options": self.CL_ALL_DISABLED,
+                    "cl-testtype-options": list(),
+                    "cl-testtype-value": list(),
+                    "cl-testtype-all-value": list(),
+                    "cl-testtype-all-options": self.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": self.CL_ALL_DISABLED,
+                    "cl-framesize-options": list(),
+                    "cl-framesize-value": list(),
+                    "cl-framesize-all-value": list(),
+                    "cl-framesize-all-options": self.CL_ALL_DISABLED,
+                    "cl-testtype-options": list(),
+                    "cl-testtype-value": list(),
+                    "cl-testtype-all-value": list(),
+                    "cl-testtype-all-options": self.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": self.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": self.CL_ALL_DISABLED,
+                    "cl-framesize-options": list(),
+                    "cl-framesize-value": list(),
+                    "cl-framesize-all-value": list(),
+                    "cl-framesize-all-options": self.CL_ALL_DISABLED,
+                    "cl-testtype-options": list(),
+                    "cl-testtype-value": list(),
+                    "cl-testtype-all-value": list(),
+                    "cl-testtype-all-options": self.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": self.CL_ALL_DISABLED,
+                    "cl-framesize-options": list(),
+                    "cl-framesize-value": list(),
+                    "cl-framesize-all-value": list(),
+                    "cl-framesize-all-options": self.CL_ALL_DISABLED,
+                    "cl-testtype-options": list(),
+                    "cl-testtype-value": list(),
+                    "cl-testtype-all-value": list(),
+                    "cl-testtype-all-options": self.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": self.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": self.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": self.CL_ALL_ENABLED,
+                    })
+            elif trigger_id == "cl-ctrl-core":
+                val_sel, val_all = self._sync_checklists(
+                    opt=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 = self._sync_checklists(
+                    opt = 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 = self._sync_checklists(
+                    opt = 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 = self._sync_checklists(
+                    opt = 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 = self._sync_checklists(
+                    opt = 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 = self._sync_checklists(
+                    opt = 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 = self.STYLE_ENABLED
+                    row_btns_sel_tests = self.STYLE_ENABLED
+                    if self.CLEAR_ALL_INPUTS:
+                        ctrl_panel.set(ctrl_panel.defaults)
+                    ctrl_panel.set({
+                        "cl-selected-options": self._list_tests(store_sel)
+                    })
+                    row_fig_tput, row_fig_tsa, row_table, row_btn_dwnld = \
+                        _generate_plotting_area(
+                            graph_iterative(self.data, store_sel, self.layout),
+                            table_comparison(self.data, store_sel),
+                            _gen_new_url(parsed_url, store_sel)
+                        )
+            elif trigger_id == "btn-sel-remove-all":
+                _ = btn_remove_all
+                row_fig_tput = self.PLACEHOLDER
+                row_fig_tsa = self.PLACEHOLDER
+                row_table = self.PLACEHOLDER
+                row_btn_dwnld = self.PLACEHOLDER
+                row_card_sel_tests = self.STYLE_DISABLED
+                row_btns_sel_tests = self.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
+                if store_sel:
+                    row_fig_tput, row_fig_tsa, row_table, row_btn_dwnld = \
+                        _generate_plotting_area(
+                            graph_iterative(self.data, store_sel, self.layout),
+                            table_comparison(self.data, store_sel),
+                            _gen_new_url(parsed_url, store_sel)
+                        )
+                    ctrl_panel.set({
+                        "cl-selected-options": self._list_tests(store_sel)
+                    })
+                else:
+                    row_fig_tput = self.PLACEHOLDER
+                    row_fig_tsa = self.PLACEHOLDER
+                    row_table = self.PLACEHOLDER
+                    row_btn_dwnld = self.PLACEHOLDER
+                    row_card_sel_tests = self.STYLE_DISABLED
+                    row_btns_sel_tests = self.STYLE_DISABLED
+                    store_sel = list()
+                    ctrl_panel.set({"cl-selected-options": list()})
+            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_fig_tput, row_fig_tsa, row_table, row_btn_dwnld = \
+                            _generate_plotting_area(
+                                graph_iterative(self.data, store_sel,
+                                    self.layout),
+                                table_comparison(self.data, store_sel),
+                                _gen_new_url(parsed_url, store_sel)
+                            )
+                        row_card_sel_tests = self.STYLE_ENABLED
+                        row_btns_sel_tests = self.STYLE_ENABLED
+                        ctrl_panel.set({
+                            "cl-selected-options": self._list_tests(store_sel)
+                        })
+                    else:
+                        row_fig_tput = self.PLACEHOLDER
+                        row_fig_tsa = self.PLACEHOLDER
+                        row_table = self.PLACEHOLDER
+                        row_btn_dwnld = self.PLACEHOLDER
+                        row_card_sel_tests = self.STYLE_DISABLED
+                        row_btns_sel_tests = self.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})
+
+            ret_val = [
+                ctrl_panel.panel, store_sel,
+                row_fig_tput, row_fig_tsa, 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("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:
+        #     """
+        #     """
+        #     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):
+        #     """
+        #     """
+
+        #     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, "trending_data.csv")
diff --git a/resources/tools/dash/app/pal/report/layout.yaml b/resources/tools/dash/app/pal/report/layout.yaml
new file mode 100644 (file)
index 0000000..6fa91f3
--- /dev/null
@@ -0,0 +1,150 @@
+plot-throughput:
+  titlefont:
+    size: 16
+  xaxis:
+    title: "<b>Test Cases [Index]</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(220, 220, 220)"
+    tickmode: "array"
+    tickfont:
+      size: 14
+    zeroline: False
+  yaxis:
+    title: "<b>Packet Throughput [Mpps]</b>"
+    titlefont:
+      size: 14
+    gridcolor: "rgb(230, 230, 230)"
+    hoverformat: ".4r"
+    tickformat: ".3r"
+    linecolor: "rgb(220, 220, 220)"
+    linewidth: 1
+    showgrid: True
+    showline: True
+    showticklabels: True
+    tickcolor: "rgb(220, 220, 220)"
+    tickfont:
+      size: 14
+    zeroline: False
+    range: [0, 50]
+  autosize: False
+  margin:
+    t: 50
+    b: 0
+    l: 80
+    r: 20
+  showlegend: True
+  legend:
+    orientation: "h"
+    font:
+      size: 14
+  width: 700
+  height: 900
+  paper_bgcolor: "#fff"
+  plot_bgcolor: "#fff"
+  hoverlabel:
+    namelength: -1
+
+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
+      },
+    },
+  ]
index 769a6dd..8330f87 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Instantiate the Report Dash application.
+"""Instantiate the Report Dash applocation.
 """
-
 import dash
-from dash import dcc
-from dash import html
-from dash import dash_table
+import dash_bootstrap_components as dbc
 
-from .data import read_stats
-from .data import read_trending_mrr, read_trending_ndrpdr
-from .data import read_iterative_mrr, read_iterative_ndrpdr
-from .layout import html_layout
+from .layout import Layout
 
 
-def init_report(server):
+def init_report(server, releases):
     """Create a Plotly Dash dashboard.
 
     :param server: Flask server.
@@ -37,72 +31,19 @@ def init_report(server):
     dash_app = dash.Dash(
         server=server,
         routes_pathname_prefix=u"/report/",
-        external_stylesheets=[
-            u"/static/dist/css/styles.css",
-            u"https://fonts.googleapis.com/css?family=Lato",
-        ],
+        external_stylesheets=[dbc.themes.LUX],
     )
 
     # Custom HTML layout
-    dash_app.index_string = html_layout
-
-    # Create Layout
-    dash_app.layout = html.Div(
-        children=[
-            html.Div(
-                children=create_data_table(
-                    read_stats().dropna(),
-                    u"database-table-stats"
-                )
-            ),
-            html.Div(
-                children=create_data_table(
-                    read_trending_mrr().dropna(),
-                    u"database-table-mrr"
-                )
-            ),
-            html.Div(
-                children=create_data_table(
-                    read_trending_ndrpdr().dropna(),
-                    u"database-table-ndrpdr"
-                )
-            ),
-            html.Div(
-                children=create_data_table(
-                    read_iterative_mrr().dropna(),
-                    u"database-table-iterative-mrr"
-                )
-            ),
-            html.Div(
-                children=create_data_table(
-                    read_iterative_ndrpdr().dropna(),
-                    u"database-table-iterative-ndrpdr"
-                )
-            )
-        ],
-        id=u"dash-container",
+    layout = Layout(
+        app=dash_app,
+        releases=releases,
+        html_layout_file="pal/templates/report_layout.jinja2",
+        graph_layout_file="pal/report/layout.yaml",
+        data_spec_file="pal/data/data.yaml",
+        tooltip_file="pal/data/tooltips.yaml"
     )
-    return dash_app.server
-
-
-def create_data_table(df, id):
-    """Create Dash datatable from Pandas DataFrame.
+    dash_app.index_string = layout.html_layout
+    dash_app.layout = layout.add_content()
 
-    DEMO
-    """
-
-    table = dash_table.DataTable(
-        id=id,
-        columns=[{u"name": i, u"id": i} for i in df.columns],
-        data=df.to_dict(u"records"),
-        fixed_rows={'headers': True},
-        sort_action=u"native",
-        sort_mode=u"native",
-        page_size=5,
-        style_header={
-            'overflow': 'hidden',
-            'textOverflow': 'ellipsis',
-            'minWidth': 95, 'maxWidth': 95, 'width': 95,
-        }
-    )
-    return table
+    return dash_app.server
index 2f43308..5c3758b 100644 (file)
@@ -45,7 +45,7 @@ class Layout:
         "color": "#135d7c"
     }
 
-    def __init__(self, app: Flask, html_layout_file: str, spec_file: str,
+    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:
         """
@@ -54,7 +54,6 @@ class Layout:
         # Inputs
         self._app = app
         self._html_layout_file = html_layout_file
-        self._spec_file = spec_file
         self._graph_layout_file = graph_layout_file
         self._data_spec_file = data_spec_file
         self._tooltip_file = tooltip_file
index 56fe27f..37a0875 100644 (file)
@@ -38,7 +38,6 @@ def init_stats(server, time_period=None):
     layout = Layout(
         app=dash_app,
         html_layout_file="pal/templates/stats_layout.jinja2",
-        spec_file="pal/stats/spec_job_selection.yaml",
         graph_layout_file="pal/stats/layout.yaml",
         data_spec_file="pal/data/data.yaml",
         tooltip_file="pal/data/tooltips.yaml",
diff --git a/resources/tools/dash/app/pal/templates/report_layout.jinja2 b/resources/tools/dash/app/pal/templates/report_layout.jinja2
new file mode 100644 (file)
index 0000000..c535d37
--- /dev/null
@@ -0,0 +1,17 @@
+<!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
index 6d9eca6..55d0d08 100644 (file)
@@ -31,15 +31,19 @@ from json import loads, JSONDecodeError
 from ast import literal_eval
 
 from ..data.data import Data
+from ..data.url_processing import url_decode, url_encode
 from .graphs import graph_trending, graph_hdrh_latency, \
     select_trending_data
-from ..data.url_processing import url_decode, url_encode
 
 
 class Layout:
     """
     """
 
+    # If True, clear all inputs in control panel when button "ADD SELECTED" is
+    # pressed.
+    CLEAR_ALL_INPUTS = False
+
     STYLE_DISABLED = {"display": "none"}
     STYLE_ENABLED = {"display": "inherit"}
 
@@ -81,7 +85,7 @@ class Layout:
         "color": "#135d7c"
     }
 
-    def __init__(self, app: Flask, html_layout_file: str, spec_file: str,
+    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:
         """
@@ -90,7 +94,6 @@ class Layout:
         # Inputs
         self._app = app
         self._html_layout_file = html_layout_file
-        self._spec_file = spec_file
         self._graph_layout_file = graph_layout_file
         self._data_spec_file = data_spec_file
         self._tooltip_file = tooltip_file
@@ -914,11 +917,9 @@ class Layout:
 
             if trigger_id == "dd-ctrl-dut":
                 try:
+                    dut = self.spec_tbs[dd_dut]
                     options = sorted(
-                        [
-                            {"label": v, "value": v}
-                                for v in self.spec_tbs[dd_dut].keys()
-                        ],
+                        [{"label": v, "value": v}for v in dut.keys()],
                         key=lambda d: d["label"]
                     )
                     disabled = False
@@ -933,6 +934,7 @@ class Layout:
                     "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(),
@@ -951,11 +953,10 @@ class Layout:
             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": self.label(v), "value": v}
-                                for v in self.spec_tbs[dut][dd_phy].keys()
-                        ],
+                        [{"label": self.label(v), "value": v}
+                            for v in phy.keys()],
                         key=lambda d: d["label"]
                     )
                     disabled = False
@@ -967,6 +968,7 @@ class Layout:
                     "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(),
@@ -986,11 +988,9 @@ class Layout:
                 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 self.spec_tbs[dut][phy][dd_area].keys()
-                        ],
+                        [{"label": v, "value": v} for v in area.keys()],
                         key=lambda d: d["label"]
                     )
                     disabled = False
@@ -1022,19 +1022,17 @@ class Layout:
                 dut = ctrl_panel.get("dd-ctrl-dut-value")
                 phy = ctrl_panel.get("dd-ctrl-phy-value")
                 area = ctrl_panel.get("dd-ctrl-area-value")
-                cores = self.spec_tbs[dut][phy][area][dd_test]["core"]
-                fsizes = self.spec_tbs[dut][phy][area][dd_test]["frame-size"]
-                ttypes = self.spec_tbs[dut][phy][area][dd_test]["test-type"]
+                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)
-                    ]
+                    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,
@@ -1153,24 +1151,22 @@ class Layout:
                     store_sel = sorted(store_sel, key=lambda d: d["id"])
                     row_card_sel_tests = self.STYLE_ENABLED
                     row_btns_sel_tests = self.STYLE_ENABLED
-                    ctrl_panel.set(ctrl_panel.defaults)
+                    if self.CLEAR_ALL_INPUTS:
+                        ctrl_panel.set(ctrl_panel.defaults)
                     ctrl_panel.set({
                         "cl-selected-options": self._list_tests(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
-                            ),
+                            graph_trending(self.data, store_sel, self.layout,
+                                d_start, d_end),
                             _gen_new_url(parsed_url, store_sel, d_start, d_end)
                         )
             elif trigger_id == "dpr-period":
                 row_fig_tput, row_fig_lat, row_btn_dwnld = \
                     _generate_plotting_area(
-                        graph_trending(
-                            self.data, store_sel, self.layout, d_start, d_end
-                        ),
+                        graph_trending(self.data, store_sel, self.layout,
+                            d_start, d_end),
                         _gen_new_url(parsed_url, store_sel, d_start, d_end)
                     )
             elif trigger_id == "btn-sel-remove-all":
@@ -1181,9 +1177,7 @@ class Layout:
                 row_card_sel_tests = self.STYLE_DISABLED
                 row_btns_sel_tests = self.STYLE_DISABLED
                 store_sel = list()
-                ctrl_panel.set({
-                        "cl-selected-options": list()
-                })
+                ctrl_panel.set({"cl-selected-options": list()})
             elif trigger_id == "btn-sel-remove":
                 _ = btn_remove
                 if list_sel:
@@ -1195,10 +1189,8 @@ class Layout:
                 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
-                            ),
+                            graph_trending(self.data, store_sel, self.layout,
+                                d_start, d_end),
                             _gen_new_url(parsed_url, store_sel, d_start, d_end)
                         )
                     ctrl_panel.set({
@@ -1211,9 +1203,7 @@ class Layout:
                     row_card_sel_tests = self.STYLE_DISABLED
                     row_btns_sel_tests = self.STYLE_DISABLED
                     store_sel = list()
-                    ctrl_panel.set({
-                        "cl-selected-options": list()
-                    })
+                    ctrl_panel.set({"cl-selected-options": list()})
             elif trigger_id == "url":
                 # TODO: Add verification
                 url_params = parsed_url["params"]
@@ -1255,9 +1245,7 @@ class Layout:
                 disabled = False
             else:
                 disabled = True
-            ctrl_panel.set({
-                "btn-ctrl-add-disabled": disabled
-            })
+            ctrl_panel.set({"btn-ctrl-add-disabled": disabled})
 
             ret_val = [
                 ctrl_panel.panel, store_sel,
index 68dc420..88b0815 100644 (file)
@@ -38,7 +38,6 @@ def init_trending(server, time_period=None):
     layout = Layout(
         app=dash_app,
         html_layout_file="pal/templates/trending_layout.jinja2",
-        spec_file="pal/trending/spec_test_selection.yaml",
         graph_layout_file="pal/trending/layout.yaml",
         data_spec_file="pal/data/data.yaml",
         tooltip_file="pal/data/tooltips.yaml",