C-Dash: Add search in tests 67/40367/16 oper-240304
authorTibor Frank <tifrank@cisco.com>
Tue, 20 Feb 2024 11:04:48 +0000 (11:04 +0000)
committerTibor Frank <tifrank@cisco.com>
Wed, 28 Feb 2024 14:15:28 +0000 (14:15 +0000)
Change-Id: Ia250c4b4e299d48bc68cf01e65fe37a281047060
Signed-off-by: Tibor Frank <tifrank@cisco.com>
44 files changed:
csit.infra.dash/app/cdash/__init__.py
csit.infra.dash/app/cdash/comparisons/__init__.py
csit.infra.dash/app/cdash/comparisons/comparisons.py
csit.infra.dash/app/cdash/comparisons/layout.py
csit.infra.dash/app/cdash/comparisons/tables.py
csit.infra.dash/app/cdash/coverage/__init__.py
csit.infra.dash/app/cdash/coverage/coverage.py
csit.infra.dash/app/cdash/coverage/layout.py
csit.infra.dash/app/cdash/coverage/tables.py
csit.infra.dash/app/cdash/data/__init__.py
csit.infra.dash/app/cdash/data/data.py
csit.infra.dash/app/cdash/news/__init__.py
csit.infra.dash/app/cdash/news/layout.py
csit.infra.dash/app/cdash/news/news.py
csit.infra.dash/app/cdash/news/tables.py
csit.infra.dash/app/cdash/report/__init__.py
csit.infra.dash/app/cdash/report/graphs.py
csit.infra.dash/app/cdash/report/layout.py
csit.infra.dash/app/cdash/report/report.py
csit.infra.dash/app/cdash/routes.py
csit.infra.dash/app/cdash/search/__init__.py [new file with mode: 0644]
csit.infra.dash/app/cdash/search/layout.py [new file with mode: 0644]
csit.infra.dash/app/cdash/search/layout.yaml [new file with mode: 0644]
csit.infra.dash/app/cdash/search/search.py [new file with mode: 0644]
csit.infra.dash/app/cdash/search/tables.py [new file with mode: 0644]
csit.infra.dash/app/cdash/stats/__init__.py
csit.infra.dash/app/cdash/stats/graphs.py
csit.infra.dash/app/cdash/stats/layout.py
csit.infra.dash/app/cdash/stats/stats.py
csit.infra.dash/app/cdash/templates/base_layout.jinja2
csit.infra.dash/app/cdash/trending/__init__.py
csit.infra.dash/app/cdash/trending/graphs.py
csit.infra.dash/app/cdash/trending/layout.py
csit.infra.dash/app/cdash/trending/trending.py
csit.infra.dash/app/cdash/utils/__init__.py
csit.infra.dash/app/cdash/utils/anomalies.py
csit.infra.dash/app/cdash/utils/constants.py
csit.infra.dash/app/cdash/utils/control_panel.py
csit.infra.dash/app/cdash/utils/telemetry_data.py
csit.infra.dash/app/cdash/utils/trigger.py
csit.infra.dash/app/cdash/utils/url_processing.py
csit.infra.dash/app/cdash/utils/utils.py
csit.infra.dash/app/config.py
csit.infra.dash/app/wsgi.py

index 796dcef..3d3f200 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -127,6 +127,17 @@ def init_app():
             from .coverage.coverage import init_coverage
             app = init_coverage(app, data_coverage=data["coverage"])
 
             from .coverage.coverage import init_coverage
             app = init_coverage(app, data_coverage=data["coverage"])
 
+        if all((data["trending"].empty, data["iterative"].empty,
+                data["coverage"].empty)):
+            logging.error((
+                f'"{C.SEARCH_TITLE}" application not loaded, '
+                'no data available.'
+            ))
+        else:
+            logging.info(C.SEARCH_TITLE)
+            from .search.search import init_search
+            app = init_search(app, data)
+
     return app
 
 
     return app
 
 
index f0d52c2..c6a5f63 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index bc42085..01319ad 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 0680cc3..45bc75a 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -28,8 +28,9 @@ from ..utils.constants import Constants as C
 from ..utils.control_panel import ControlPanel
 from ..utils.trigger import Trigger
 from ..utils.url_processing import url_decode
 from ..utils.control_panel import ControlPanel
 from ..utils.trigger import Trigger
 from ..utils.url_processing import url_decode
-from ..utils.utils import generate_options, gen_new_url
-from .tables import comparison_table, filter_table_data
+from ..utils.utils import generate_options, gen_new_url, navbar_report, \
+    filter_table_data
+from .tables import comparison_table
 
 
 # Control panel partameters and their default values.
 
 
 # Control panel partameters and their default values.
@@ -194,9 +195,7 @@ class Layout:
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
-                        children=[
-                            self._add_navbar()
-                        ]
+                        children=[navbar_report((False, True, False, False)), ]
                     ),
                     dbc.Row(
                         id="row-main",
                     ),
                     dbc.Row(
                         id="row-main",
@@ -238,43 +237,6 @@ class Layout:
                 ]
             )
 
                 ]
             )
 
-    def _add_navbar(self):
-        """Add nav element with navigation panel. It is placed on the top.
-
-        :returns: Navigation bar.
-        :rtype: dbc.NavbarSimple
-        """
-        return dbc.NavbarSimple(
-            id="navbarsimple-main",
-            children=[
-                dbc.NavItem(dbc.NavLink(
-                    C.REPORT_TITLE,
-                    external_link=True,
-                    href="/report"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Comparisons",
-                    active=True,
-                    external_link=True,
-                    href="/comparisons"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Coverage Data",
-                    external_link=True,
-                    href="/coverage"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Documentation",
-                    id="btn-documentation",
-                ))
-            ],
-            brand=C.BRAND,
-            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.
 
     def _add_ctrl_col(self) -> dbc.Col:
         """Add column with controls. It is placed on the left side.
 
@@ -539,7 +501,7 @@ class Layout:
         ) -> list:
         """Generate the plotting area with all its content.
 
         ) -> list:
         """Generate the plotting area with all its content.
 
-        :param title: The title of the comparison table..
+        :param title: The title of the comparison table.
         :param table: Comparison table to be displayed.
         :param url: URL to be displayed in the modal window.
         :type title: str
         :param table: Comparison table to be displayed.
         :param url: URL to be displayed in the modal window.
         :type title: str
index 8c19d3c..ab99f18 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -309,65 +309,3 @@ def comparison_table(
     )
 
     return (title, df_cmp)
     )
 
     return (title, df_cmp)
-
-
-def filter_table_data(
-        store_table_data: list,
-        table_filter: str
-    ) -> list:
-    """Filter table data using user specified filter.
-
-    :param store_table_data: Table data represented as a list of records.
-    :param table_filter: User specified filter.
-    :type store_table_data: list
-    :type table_filter: str
-    :returns: A new table created by filtering of table data represented as
-        a list of records.
-    :rtype: list
-    """
-
-    # Checks:
-    if not any((table_filter, store_table_data, )):
-        return store_table_data
-
-    def _split_filter_part(filter_part: str) -> tuple:
-        """Split a part of filter into column name, operator and value.
-        A "part of filter" is a sting berween "&&" operator.
-
-        :param filter_part: A part of filter.
-        :type filter_part: str
-        :returns: Column name, operator, value
-        :rtype: tuple[str, str, str|float]
-        """
-        for operator_type in C.OPERATORS:
-            for operator in operator_type:
-                if operator in filter_part:
-                    name_p, val_p = filter_part.split(operator, 1)
-                    name = name_p[name_p.find("{") + 1 : name_p.rfind("}")]
-                    val_p = val_p.strip()
-                    if (val_p[0] == val_p[-1] and val_p[0] in ("'", '"', '`')):
-                        value = val_p[1:-1].replace("\\" + val_p[0], val_p[0])
-                    else:
-                        try:
-                            value = float(val_p)
-                        except ValueError:
-                            value = val_p
-
-                    return name, operator_type[0].strip(), value
-        return (None, None, None)
-
-    df = pd.DataFrame.from_records(store_table_data)
-    for filter_part in table_filter.split(" && "):
-        col_name, operator, filter_value = _split_filter_part(filter_part)
-        if operator == "contains":
-            df = df.loc[df[col_name].str.contains(filter_value, regex=True)]
-        elif operator in ("eq", "ne", "lt", "le", "gt", "ge"):
-            # These operators match pandas series operator method names.
-            df = df.loc[getattr(df[col_name], operator)(filter_value)]
-        elif operator == "datestartswith":
-            # This is a simplification of the front-end filtering logic,
-            # only works with complete fields in standard format.
-            # Currently not used in comparison tables.
-            df = df.loc[df[col_name].str.startswith(filter_value)]
-
-    return df.to_dict("records")
index f0d52c2..c6a5f63 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 4dfd7a8..3388d48 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index a2d51d4..8ebda5e 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -29,7 +29,7 @@ from ast import literal_eval
 from ..utils.constants import Constants as C
 from ..utils.control_panel import ControlPanel
 from ..utils.trigger import Trigger
 from ..utils.constants import Constants as C
 from ..utils.control_panel import ControlPanel
 from ..utils.trigger import Trigger
-from ..utils.utils import label, gen_new_url, generate_options
+from ..utils.utils import label, gen_new_url, generate_options, navbar_report
 from ..utils.url_processing import url_decode
 from .tables import coverage_tables, select_coverage_data
 
 from ..utils.url_processing import url_decode
 from .tables import coverage_tables, select_coverage_data
 
@@ -161,9 +161,7 @@ class Layout:
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
-                        children=[
-                            self._add_navbar()
-                        ]
+                        children=[navbar_report((False, False, True, False)), ]
                     ),
                     dbc.Row(
                         id="row-main",
                     ),
                     dbc.Row(
                         id="row-main",
@@ -203,43 +201,6 @@ class Layout:
                 ]
             )
 
                 ]
             )
 
-    def _add_navbar(self):
-        """Add nav element with navigation panel. It is placed on the top.
-
-        :returns: Navigation bar.
-        :rtype: dbc.NavbarSimple
-        """
-        return dbc.NavbarSimple(
-            id="navbarsimple-main",
-            children=[
-                dbc.NavItem(dbc.NavLink(
-                    C.REPORT_TITLE,
-                    external_link=True,
-                    href="/report"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Comparisons",
-                    external_link=True,
-                    href="/comparisons"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Coverage Data",
-                    active=True,
-                    external_link=True,
-                    href="/coverage"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Documentation",
-                    id="btn-documentation",
-                ))
-            ],
-            brand=C.BRAND,
-            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.
 
     def _add_ctrl_col(self) -> dbc.Col:
         """Add column with controls. It is placed on the left side.
 
index 372a820..84adb09 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -57,7 +57,7 @@ def select_coverage_data(
         topo, arch, nic, drv = phy
         drv_str = "" if drv == "dpdk" else drv.replace("_", "-")
     else:
         topo, arch, nic, drv = phy
         drv_str = "" if drv == "dpdk" else drv.replace("_", "-")
     else:
-        return l_data
+        return l_data, None
 
     df = pd.DataFrame(data.loc[(
         (data["passed"] == True) &
 
     df = pd.DataFrame(data.loc[(
         (data["passed"] == True) &
@@ -78,8 +78,10 @@ def select_coverage_data(
                 df[df.test_id.str.contains(f"-{driver}-")].index,
                 inplace=True
             )
                 df[df.test_id.str.contains(f"-{driver}-")].index,
                 inplace=True
             )
-
-    ttype = df["test_type"].to_list()[0]
+    try:
+        ttype = df["test_type"].to_list()[0]
+    except IndexError:
+        return l_data, None
 
     # Prepare the coverage data
     def _latency(hdrh_string: str, percentile: float) -> int:
 
     # Prepare the coverage data
     def _latency(hdrh_string: str, percentile: float) -> int:
@@ -177,16 +179,20 @@ def select_coverage_data(
 def coverage_tables(
         data: pd.DataFrame,
         selected: dict,
 def coverage_tables(
         data: pd.DataFrame,
         selected: dict,
-        show_latency: bool=True
-    ) -> list:
+        show_latency: bool=True,
+        start_collapsed: bool=True
+    ) -> dbc.Accordion:
     """Generate an accordion with coverage tables.
 
     :param data: Coverage data.
     :param selected: Dictionary with user selection.
     :param show_latency: If True, latency is displayed in the tables.
     """Generate an accordion with coverage tables.
 
     :param data: Coverage data.
     :param selected: Dictionary with user selection.
     :param show_latency: If True, latency is displayed in the tables.
+    :param start_collapsed: If True, the accordion with tables is collapsed when
+        displayed.
     :type data: pandas.DataFrame
     :type selected: dict
     :type show_latency: bool
     :type data: pandas.DataFrame
     :type selected: dict
     :type show_latency: bool
+    :type start_collapsed: bool
     :returns: Accordion with suite names (titles) and tables.
     :rtype: dash_bootstrap_components.Accordion
     """
     :returns: Accordion with suite names (titles) and tables.
     :rtype: dash_bootstrap_components.Accordion
     """
@@ -295,9 +301,15 @@ def coverage_tables(
                 )
             )
         )
                 )
             )
         )
+    if not accordion_items:
+        accordion_items.append(dbc.AccordionItem(
+            title="No data.",
+            children="No data."
+        ))
+        start_collapsed = True
     return dbc.Accordion(
         children=accordion_items,
         class_name="gy-1 p-0",
     return dbc.Accordion(
         children=accordion_items,
         class_name="gy-1 p-0",
-        start_collapsed=True,
+        start_collapsed=start_collapsed,
         always_open=True
     )
         always_open=True
     )
index f0d52c2..c6a5f63 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index ce98476..2c49992 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index f0d52c2..c6a5f63 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 3f2280e..b40db48 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -24,7 +24,7 @@ from dash import callback_context
 from dash import Input, Output, State
 
 from ..utils.constants import Constants as C
 from dash import Input, Output, State
 
 from ..utils.constants import Constants as C
-from ..utils.utils import gen_new_url
+from ..utils.utils import gen_new_url, navbar_trending
 from ..utils.anomalies import classify_anomalies
 from ..utils.url_processing import url_decode
 from .tables import table_summary
 from ..utils.anomalies import classify_anomalies
 from ..utils.url_processing import url_decode
 from .tables import table_summary
@@ -262,9 +262,7 @@ class Layout:
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
-                        children=[
-                            self._add_navbar()
-                        ]
+                        children=[navbar_trending((False, True, False, False))]
                     ),
                     dbc.Row(
                         id="row-main",
                     ),
                     dbc.Row(
                         id="row-main",
@@ -301,44 +299,6 @@ class Layout:
                 ]
             )
 
                 ]
             )
 
-    def _add_navbar(self):
-        """Add nav element with navigation panel. It is placed on the top.
-
-        :returns: Navigation bar.
-        :rtype: dbc.NavbarSimple
-        """
-
-        return dbc.NavbarSimple(
-            id="navbarsimple-main",
-            children=[
-                dbc.NavItem(dbc.NavLink(
-                    C.TREND_TITLE,
-                    external_link=True,
-                    href="/trending"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    C.NEWS_TITLE,
-                    active=True,
-                    external_link=True,
-                    href="/news"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    C.STATS_TITLE,
-                    external_link=True,
-                    href="/stats"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Documentation",
-                    id="btn-documentation",
-                ))
-            ],
-            brand=C.BRAND,
-            brand_href="/",
-            brand_external_link=True,
-            class_name="p-2",
-            fluid=True
-        )
-
     def _add_ctrl_col(self) -> dbc.Col:
         """Add column with control panel. It is placed on the left side.
 
     def _add_ctrl_col(self) -> dbc.Col:
         """Add column with control panel. It is placed on the left side.
 
@@ -527,8 +487,8 @@ class Layout:
 
         @app.callback(
             Output("plot-mod-url", "is_open"),
 
         @app.callback(
             Output("plot-mod-url", "is_open"),
-            [Input("plot-btn-url", "n_clicks")],
-            [State("plot-mod-url", "is_open")],
+            Input("plot-btn-url", "n_clicks"),
+            State("plot-mod-url", "is_open")
         )
         def toggle_plot_mod_url(n, is_open):
             """Toggle the modal window with url.
         )
         def toggle_plot_mod_url(n, is_open):
             """Toggle the modal window with url.
index b5cc548..747facc 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index c84f84e..1e9aefa 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index f0d52c2..c6a5f63 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 50a3be2..44c57d4 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -73,8 +73,8 @@ def select_iterative_data(data: pd.DataFrame, itm:dict) -> pd.DataFrame:
     return df
 
 
     return df
 
 
-def graph_iterative(data: pd.DataFrame, sel:dict, layout: dict,
-        normalize: bool) -> tuple:
+def graph_iterative(data: pd.DataFrame, sel: list, layout: dict,
+        normalize: bool=False) -> tuple:
     """Generate the statistical box graph with iterative data (MRR, NDR and PDR,
     for PDR also Latencies).
 
     """Generate the statistical box graph with iterative data (MRR, NDR and PDR,
     for PDR also Latencies).
 
@@ -84,7 +84,7 @@ def graph_iterative(data: pd.DataFrame, sel:dict, layout: dict,
     :param normalize: If True, the data is normalized to CPU frequency
         Constants.NORM_FREQUENCY.
     :param data: pandas.DataFrame
     :param normalize: If True, the data is normalized to CPU frequency
         Constants.NORM_FREQUENCY.
     :param data: pandas.DataFrame
-    :param sel: dict
+    :param sel: list
     :param layout: dict
     :param normalize: bool
     :returns: Tuple of graphs - throughput and latency.
     :param layout: dict
     :param normalize: bool
     :returns: Tuple of graphs - throughput and latency.
index 08a430b..400fd60 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -32,7 +32,8 @@ from ..utils.constants import Constants as C
 from ..utils.control_panel import ControlPanel
 from ..utils.trigger import Trigger
 from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
 from ..utils.control_panel import ControlPanel
 from ..utils.trigger import Trigger
 from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
-    generate_options, get_list_group_items, graph_hdrh_latency
+    generate_options, get_list_group_items, navbar_report, \
+    show_iterative_graph_data
 from ..utils.url_processing import url_decode
 from .graphs import graph_iterative, select_iterative_data
 
 from ..utils.url_processing import url_decode
 from .graphs import graph_iterative, select_iterative_data
 
@@ -250,9 +251,7 @@ class Layout:
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
-                        children=[
-                            self._add_navbar()
-                        ]
+                        children=[navbar_report((True, False, False, False)), ]
                     ),
                     dbc.Row(
                         id="row-main",
                     ),
                     dbc.Row(
                         id="row-main",
@@ -306,43 +305,6 @@ class Layout:
                 ]
             )
 
                 ]
             )
 
-    def _add_navbar(self):
-        """Add nav element with navigation panel. It is placed on the top.
-
-        :returns: Navigation bar.
-        :rtype: dbc.NavbarSimple
-        """
-        return dbc.NavbarSimple(
-            id="navbarsimple-main",
-            children=[
-                dbc.NavItem(dbc.NavLink(
-                    C.REPORT_TITLE,
-                    active=True,
-                    external_link=True,
-                    href="/report"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Comparisons",
-                    external_link=True,
-                    href="/comparisons"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Coverage Data",
-                    external_link=True,
-                    href="/coverage"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Documentation",
-                    id="btn-documentation",
-                ))
-            ],
-            brand=C.BRAND,
-            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.
 
     def _add_ctrl_col(self) -> dbc.Col:
         """Add column with controls. It is placed on the left side.
 
@@ -1365,118 +1327,11 @@ class Layout:
             """
 
             trigger = Trigger(callback_context.triggered)
             """
 
             trigger = Trigger(callback_context.triggered)
-
-            if trigger.idx == "tput":
-                idx = 0
-            elif trigger.idx == "bandwidth":
-                idx = 1
-            elif trigger.idx == "lat":
-                idx = len(graph_data) - 1
-            else:
-                return list(), list(), False
-
-            try:
-                graph_data = graph_data[idx]["points"]
-            except (IndexError, KeyError, ValueError, TypeError):
+            if not trigger.value:
                 raise PreventUpdate
 
                 raise PreventUpdate
 
-            def _process_stats(data: list, param: str) -> list:
-                """Process statistical data provided by plot.ly box graph.
-
-                :param data: Statistical data provided by plot.ly box graph.
-                :param param: Parameter saying if the data come from "tput" or
-                    "lat" graph.
-                :type data: list
-                :type param: str
-                :returns: Listo of tuples where the first value is the
-                    statistic's name and the secont one it's value.
-                :rtype: list
-                """
-                if len(data) == 7:
-                    stats = ("max", "upper fence", "q3", "median", "q1",
-                            "lower fence", "min")
-                elif len(data) == 9:
-                    stats = ("outlier", "max", "upper fence", "q3", "median",
-                            "q1", "lower fence", "min", "outlier")
-                elif len(data) == 1:
-                    if param == "lat":
-                        stats = ("average latency at 50% PDR", )
-                    elif param == "bandwidth":
-                        stats = ("bandwidth", )
-                    else:
-                        stats = ("throughput", )
-                else:
-                    return list()
-                unit = " [us]" if param == "lat" else str()
-                return [(f"{stat}{unit}", f"{value['y']:,.0f}")
-                        for stat, value in zip(stats, data)]
-
-            customdata = graph_data[0].get("customdata", dict())
-            datapoint = customdata.get("metadata", dict())
-            hdrh_data = customdata.get("hdrh", dict())
-
-            list_group_items = list()
-            for k, v in datapoint.items():
-                if k == "csit-ref":
-                    if len(graph_data) > 1:
-                        continue
-                    list_group_item = dbc.ListGroupItem([
-                        dbc.Badge(k),
-                        html.A(v, href=f"{C.URL_JENKINS}{v}", target="_blank")
-                    ])
-                else:
-                    list_group_item = dbc.ListGroupItem([dbc.Badge(k), v])
-                list_group_items.append(list_group_item)
-
-            graph = list()
-            if trigger.idx == "tput":
-                title = "Throughput"
-            elif trigger.idx == "bandwidth":
-                title = "Bandwidth"
-            elif trigger.idx == "lat":
-                title = "Latency"
-                if len(graph_data) == 1:
-                    if hdrh_data:
-                        graph = [dbc.Card(
-                            class_name="gy-2 p-0",
-                            children=[
-                                dbc.CardHeader(hdrh_data.pop("name")),
-                                dbc.CardBody(dcc.Graph(
-                                    id="hdrh-latency-graph",
-                                    figure=graph_hdrh_latency(
-                                        hdrh_data, self._graph_layout
-                                    )
-                                ))
-                            ])
-                        ]
-            else:
-                raise PreventUpdate
-
-            for k, v in _process_stats(graph_data, trigger.idx):
-                list_group_items.append(dbc.ListGroupItem([dbc.Badge(k), v]))
-
-            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(
-                            dbc.ListGroup(list_group_items, flush=True),
-                            id="tput-lat-metadata",
-                            class_name="p-0"
-                        )
-                    ]
-                )
-            ]
-
-            return metadata, graph, True
+            return show_iterative_graph_data(
+                    trigger, graph_data, self._graph_layout)
 
         @app.callback(
             Output("offcanvas-documentation", "is_open"),
 
         @app.callback(
             Output("offcanvas-documentation", "is_open"),
index 661bb2c..ce5e977 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 301738c..ed29fff 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -33,5 +33,6 @@ def home():
         comp_title=C.COMP_TITLE,
         stats_title=C.STATS_TITLE,
         news_title=C.NEWS_TITLE,
         comp_title=C.COMP_TITLE,
         stats_title=C.STATS_TITLE,
         news_title=C.NEWS_TITLE,
-        cov_title=C.COVERAGE_TITLE
+        cov_title=C.COVERAGE_TITLE,
+        search_title=C.SEARCH_TITLE
     )
     )
diff --git a/csit.infra.dash/app/cdash/search/__init__.py b/csit.infra.dash/app/cdash/search/__init__.py
new file mode 100644 (file)
index 0000000..c6a5f63
--- /dev/null
@@ -0,0 +1,12 @@
+# Copyright (c) 2024 Cisco and/or its affiliates.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/csit.infra.dash/app/cdash/search/layout.py b/csit.infra.dash/app/cdash/search/layout.py
new file mode 100644 (file)
index 0000000..2c50fba
--- /dev/null
@@ -0,0 +1,928 @@
+# Copyright (c) 2024 Cisco and/or its affiliates.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""Plotly Dash HTML layout override.
+"""
+
+import logging
+import pandas as pd
+import dash_bootstrap_components as dbc
+
+from flask import Flask
+from dash import dcc
+from dash import html, dash_table
+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 ast import literal_eval
+
+from ..utils.constants import Constants as C
+from ..utils.control_panel import ControlPanel
+from ..utils.trigger import Trigger
+from ..utils.utils import gen_new_url, generate_options, navbar_trending, \
+    filter_table_data, show_trending_graph_data, show_iterative_graph_data
+from ..utils.url_processing import url_decode
+from .tables import search_table
+from ..coverage.tables import coverage_tables
+from ..report.graphs import graph_iterative
+from ..trending.graphs import graph_trending
+
+
+# Control panel partameters and their default values.
+CP_PARAMS = {
+    "datatype-val": str(),
+    "dut-opt": list(),
+    "dut-dis": C.STYLE_DONT_DISPLAY,
+    "dut-val": str(),
+    "release-opt": list(),
+    "release-dis": C.STYLE_DONT_DISPLAY,
+    "release-val": str(),
+    "help-dis": C.STYLE_DONT_DISPLAY,
+    "help-val": str(),
+    "search-dis": C.STYLE_DONT_DISPLAY,
+    "search-val": str()
+}
+
+
+class Layout:
+    """The layout of the dash app and the callbacks.
+    """
+
+    def __init__(self,
+            app: Flask,
+            data: dict,
+            html_layout_file: str,
+            graph_layout_file: str,
+            tooltip_file: str
+        ) -> None:
+        """Initialization:
+        - save the input parameters,
+        - read and pre-process the data,
+        - prepare data for the control panel,
+        - read HTML layout file,
+        - read graph layout file,
+        - read tooltips from the tooltip file.
+
+        :param app: Flask application running the dash application.
+        :param data_trending: Pandas dataframe with trending data.
+        :param html_layout_file: Path and name of the file specifying the HTML
+            layout of the dash application.
+        :param graph_layout_file: Path and name of the file with layout of
+            plot.ly graphs.
+        :param tooltip_file: Path and name of the yaml file specifying the
+            tooltips.
+        :type app: Flask
+        :type data_trending: pandas.DataFrame
+        :type html_layout_file: str
+        :type graph_layout_file: str
+        :type tooltip_file: str
+        """
+
+        # Inputs
+        self._app = app
+        self._html_layout_file = html_layout_file
+        self._graph_layout_file = graph_layout_file
+        self._tooltip_file = tooltip_file
+        # Inputs - Data
+        self._data = {
+            k: v for k, v in data.items() if not v.empty and k != "statistics"
+        }
+
+        for data_type, pd in self._data.items():
+            if pd.empty:
+                continue
+            full_id = list()
+
+            for _, row in pd.iterrows():
+                l_id = row["test_id"].split(".")
+                suite = l_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
+                    replace("2n-", "")
+                tb = "-".join(row["job"].split("-")[-2:])
+                nic = suite.split("-")[0]
+                for driver in C.DRIVERS:
+                    if driver in suite:
+                        drv = driver
+                        break
+                else:
+                    drv = "dpdk"
+                test = l_id[-1]
+
+                if data_type in ("iterative", "coverage", ):
+                    full_id.append(
+                        "_".join((row["release"], row["dut_type"],
+                            row["dut_version"], tb, nic, drv, test))
+                    )
+                else:  # Trending
+                    full_id.append(
+                        "_".join((row["dut_type"], tb, nic, drv, test))
+                    )
+            pd["full_id"] = full_id
+
+        # Get structure of tests:
+        self._duts = dict()
+        for data_type, pd in self._data.items():
+            if pd.empty:
+                continue
+            self._duts[data_type] = dict()
+            if data_type in ("iterative", "coverage", ):
+                cols = ["job", "dut_type", "dut_version", "release", "test_id"]
+                for _, row in pd[cols].drop_duplicates().iterrows():
+                    dut = row["dut_type"]
+                    if self._duts[data_type].get(dut, None) is None:
+                        self._duts[data_type][dut] = list()
+                    if row["release"] not in self._duts[data_type][dut]:
+                        self._duts[data_type][dut].append(row["release"])
+            else:
+                for dut in pd["dut_type"].unique():
+                    if self._duts[data_type].get(dut, None) is None:
+                        self._duts[data_type][dut] = list()
+
+        # Read from files:
+        self._html_layout = str()
+        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
+
+    def add_content(self):
+        """Top level method which generated the web page.
+
+        It generates:
+        - Store for user input data,
+        - Navigation bar,
+        - Main area with control panel and ploting area.
+
+        If no HTML layout is provided, an error message is displayed instead.
+
+        :returns: The HTML div with the whole page.
+        :rtype: html.Div
+        """
+        if self.html_layout and self._duts:
+            return html.Div(
+                id="div-main",
+                className="small",
+                children=[
+                    dcc.Store(id="store"),
+                    dcc.Store(id="store-table-data"),
+                    dcc.Store(id="store-filtered-table-data"),
+                    dcc.Location(id="url", refresh=False),
+                    dbc.Row(
+                        id="row-navbar",
+                        class_name="g-0",
+                        children=[navbar_trending((False, False, False, True))]
+                    ),
+                    dbc.Row(
+                        id="row-main",
+                        class_name="g-0",
+                        children=[
+                            self._add_ctrl_col(),
+                            self._add_plotting_col()
+                        ]
+                    ),
+                    dbc.Spinner(
+                        dbc.Offcanvas(
+                            class_name="w-75",
+                            id="offcanvas-details",
+                            title="Test Details",
+                            placement="end",
+                            is_open=False,
+                            children=[]
+                        ),
+                        delay_show=C.SPINNER_DELAY
+                    ),
+                    dbc.Spinner(
+                        dbc.Offcanvas(
+                            class_name="w-50",
+                            id="offcanvas-metadata",
+                            title="Detailed Information",
+                            placement="end",
+                            is_open=False,
+                            children=[
+                                dbc.Row(id="metadata-tput-lat"),
+                                dbc.Row(id="metadata-hdrh-graph")
+                            ]
+                        ),
+                        delay_show=C.SPINNER_DELAY
+                    ),
+                    dbc.Offcanvas(
+                        class_name="w-75",
+                        id="offcanvas-documentation",
+                        title="Documentation",
+                        placement="end",
+                        is_open=False,
+                        children=html.Iframe(
+                            src=C.URL_DOC_TRENDING,
+                            width="100%",
+                            height="100%"
+                        )
+                    )
+                ]
+            )
+        else:
+            return html.Div(
+                dbc.Alert("An Error Occured", color="danger"),
+                id="div-main-error"
+            )
+
+    def _add_ctrl_col(self) -> dbc.Col:
+        """Add column with controls. It is placed on the left side.
+
+        :returns: Column with the control panel.
+        :rtype: dbc.Col
+        """
+        return dbc.Col(html.Div(self._add_ctrl_panel(), className="sticky-top"))
+
+    def _add_ctrl_panel(self) -> list:
+        """Add control panel.
+
+        :returns: Control panel.
+        :rtype: list
+        """
+        return [
+            dbc.Row(
+                class_name="g-0 p-1",
+                children=[
+                    dbc.InputGroup(
+                        [
+                            dbc.InputGroupText("Data Type"),
+                            dbc.Select(
+                                id={"type": "ctrl-dd", "index": "datatype"},
+                                placeholder="Select a Data Type...",
+                                options=sorted(
+                                    [
+                                        {"label": k, "value": k} \
+                                            for k in self._data.keys()
+                                    ],
+                                    key=lambda d: d["label"]
+                                )
+                            )
+                        ],
+                        size="sm"
+                    )
+                ],
+                style=C.STYLE_DISPLAY
+            ),
+            dbc.Row(
+                class_name="g-0 p-1",
+                id={"type": "ctrl-row", "index": "dut"},
+                children=[
+                    dbc.InputGroup(
+                        [
+                            dbc.InputGroupText("DUT"),
+                            dbc.Select(
+                                id={"type": "ctrl-dd", "index": "dut"},
+                                placeholder="Select a Device under Test..."
+                            )
+                        ],
+                        size="sm"
+                    )
+                ],
+                style=C.STYLE_DONT_DISPLAY
+            ),
+            dbc.Row(
+                class_name="g-0 p-1",
+                id={"type": "ctrl-row", "index": "release"},
+                children=[
+                    dbc.InputGroup(
+                        [
+                            dbc.InputGroupText("Release"),
+                            dbc.Select(
+                                id={"type": "ctrl-dd", "index": "release"},
+                                placeholder="Select a Release..."
+                            )
+                        ],
+                        size="sm"
+                    )
+                ],
+                style=C.STYLE_DONT_DISPLAY
+            ),
+            dbc.Row(
+                class_name="g-0 p-1",
+                id={"type": "ctrl-row", "index": "help"},
+                children=[
+                    dbc.Input(
+                        id={"type": "ctrl-dd", "index": "help"},
+                        readonly=True,
+                        debounce=True,
+                        size="sm"
+                    )
+                ],
+                style=C.STYLE_DONT_DISPLAY
+            ),
+            dbc.Row(
+                class_name="g-0 p-1",
+                id={"type": "ctrl-row", "index": "search"},
+                children=[
+                    dbc.Input(
+                        id={"type": "ctrl-dd", "index": "search"},
+                        placeholder="Type a Regular Expression...",
+                        debounce=True,
+                        size="sm"
+                    )
+                ],
+                style=C.STYLE_DONT_DISPLAY
+            )
+        ]
+
+    def _add_plotting_col(self) -> dbc.Col:
+        """Add column with tables. It is placed on the right side.
+
+        :returns: Column with tables.
+        :rtype: dbc.Col
+        """
+        return dbc.Col(
+            id="col-plotting-area",
+            children=[
+                dbc.Spinner(
+                    children=[
+                        dbc.Row(
+                            id="plotting-area",
+                            class_name="g-0 p-0",
+                            children=[C.PLACEHOLDER, ]
+                        )
+                    ]
+                )
+            ],
+            width=9
+        )
+
+    @staticmethod
+    def _get_plotting_area(table: pd.DataFrame, url: str) -> list:
+        """Generate the plotting area with all its content.
+
+        :param table: Search table to be displayed.
+        :param url: URL to be displayed in a modal window.
+        :type table: pandas.DataFrame
+        :type url: str
+        :returns: List of rows with elements to be displayed in the plotting
+            area.
+        :rtype: list
+        """
+
+        if table.empty:
+            return dbc.Row(
+                dbc.Col(
+                    children=dbc.Alert(
+                        "No data found.",
+                        color="danger"
+                    ),
+                    class_name="g-0 p-1",
+                ),
+                class_name="g-0 p-0"
+            )
+
+        columns = [{"name": col, "id": col} for col in table.columns]
+
+        return [
+            dbc.Row(
+                children=[
+                    dbc.Col(
+                        children=dash_table.DataTable(
+                            id={"type": "table", "index": "search"},
+                            columns=columns,
+                            data=table.to_dict("records"),
+                            filter_action="custom",
+                            sort_action="native",
+                            sort_mode="multi",
+                            selected_columns=[],
+                            selected_rows=[],
+                            page_action="none",
+                            style_cell={"textAlign": "left"}
+                        ),
+                        class_name="g-0 p-1"
+                    )
+                ],
+                class_name="g-0 p-0"
+            ),
+            dbc.Row(
+                [
+                    dbc.Col([html.Div(
+                        [
+                            dbc.Button(
+                                id="plot-btn-url",
+                                children="Show URL",
+                                class_name="me-1",
+                                color="info",
+                                style={
+                                    "text-transform": "none",
+                                    "padding": "0rem 1rem"
+                                }
+                            ),
+                            dbc.Modal(
+                                [
+                                    dbc.ModalHeader(dbc.ModalTitle("URL")),
+                                    dbc.ModalBody(url)
+                                ],
+                                id="plot-mod-url",
+                                size="xl",
+                                is_open=False,
+                                scrollable=True
+                            ),
+                            dbc.Button(
+                                id="plot-btn-download",
+                                children="Download Data",
+                                class_name="me-1",
+                                color="info",
+                                style={
+                                    "text-transform": "none",
+                                    "padding": "0rem 1rem"
+                                }
+                            ),
+                            dcc.Download(id="download-data")
+                        ],
+                        className=\
+                            "d-grid gap-0 d-md-flex justify-content-md-end"
+                    )])
+                ],
+                class_name="g-0 p-0"
+            ),
+            dbc.Row(
+                children=C.PLACEHOLDER,
+                class_name="g-0 p-1"
+            )
+        ]
+
+    def callbacks(self, app):
+        """Callbacks for the whole application.
+
+        :param app: The application.
+        :type app: Flask
+        """
+
+        @app.callback(
+            Output("store", "data"),
+            Output("store-table-data", "data"),
+            Output("store-filtered-table-data", "data"),
+            Output("plotting-area", "children"),
+            Output({"type": "table", "index": ALL}, "data"),
+            Output({"type": "ctrl-dd", "index": "datatype"}, "value"),
+            Output({"type": "ctrl-dd", "index": "dut"}, "options"),
+            Output({"type": "ctrl-row", "index": "dut"}, "style"),
+            Output({"type": "ctrl-dd", "index": "dut"}, "value"),
+            Output({"type": "ctrl-dd", "index": "release"}, "options"),
+            Output({"type": "ctrl-row", "index": "release"}, "style"),
+            Output({"type": "ctrl-dd", "index": "release"}, "value"),
+            Output({"type": "ctrl-row", "index": "help"}, "style"),
+            Output({"type": "ctrl-dd", "index": "help"}, "value"),
+            Output({"type": "ctrl-row", "index": "search"}, "style"),
+            Output({"type": "ctrl-dd", "index": "search"}, "value"),
+            State("store", "data"),
+            State("store-table-data", "data"),
+            State("store-filtered-table-data", "data"),
+            State({"type": "table", "index": ALL}, "data"),
+            Input("url", "href"),
+            Input({"type": "table", "index": ALL}, "filter_query"),
+            Input({"type": "ctrl-dd", "index": ALL}, "value"),
+            prevent_initial_call=True
+        )
+        def _update_application(
+                store: dict,
+                store_table_data: list,
+                filtered_data: list,
+                table_data: list,
+                href: str,
+                *_
+            ) -> tuple:
+            """Update the application when the event is detected.
+            """
+
+            if store is None:
+                store = {
+                    "control-panel": dict(),
+                    "selection": dict()
+                }
+
+            ctrl_panel = ControlPanel(
+                CP_PARAMS,
+                store.get("control-panel", dict())
+            )
+            selection = store["selection"]
+
+            plotting_area = no_update
+            on_draw = False
+
+            # Parse the url:
+            parsed_url = url_decode(href)
+            if parsed_url:
+                url_params = parsed_url["params"]
+            else:
+                url_params = None
+
+            trigger = Trigger(callback_context.triggered)
+            if trigger.type == "url" and url_params:
+                try:
+                    selection = literal_eval(url_params["selection"][0])
+                    if selection:
+                        dtype = selection["datatype"]
+                        dut = selection["dut"]
+                        if dtype == "trending":
+                            rls_opts = list()
+                            rls_dis = C.STYLE_DONT_DISPLAY
+                        else:
+                            rls_opts = generate_options(self._duts[dtype][dut])
+                            rls_dis = C.STYLE_DISPLAY
+                        ctrl_panel.set({
+                            "datatype-val": dtype,
+                            "dut-opt": \
+                                generate_options(self._duts[dtype].keys()),
+                            "dut-dis": C.STYLE_DISPLAY,
+                            "dut-val": dut,
+                            "release-opt": rls_opts,
+                            "release-dis": rls_dis,
+                            "release-val": selection["release"],
+                            "help-dis": C.STYLE_DISPLAY,
+                            "help-val": selection["help"],
+                            "search-dis": C.STYLE_DISPLAY,
+                            "search-val": selection["regexp"]
+                        })
+                        on_draw = True
+                except (KeyError, IndexError, AttributeError, ValueError):
+                    pass
+            elif trigger.type == "ctrl-dd":
+                if trigger.idx == "datatype":
+                    try:
+                        data_type = self._duts[trigger.value]
+                        options = generate_options(data_type.keys())
+                        disabled = C.STYLE_DISPLAY
+                    except KeyError:
+                        options = list()
+                        disabled = C.STYLE_DONT_DISPLAY
+                    ctrl_panel.set({
+                        "datatype-val": trigger.value,
+                        "dut-opt": options,
+                        "dut-dis": disabled,
+                        "dut-val": str(),
+                        "release-opt": list(),
+                        "release-dis": C.STYLE_DONT_DISPLAY,
+                        "release-val": str(),
+                        "help-dis": C.STYLE_DONT_DISPLAY,
+                        "help-val": str(),
+                        "search-dis": C.STYLE_DONT_DISPLAY,
+                        "search-val": str()
+                    })
+                elif trigger.idx == "dut":
+                    try:
+                        data_type = ctrl_panel.get("datatype-val")
+                        dut = self._duts[data_type][trigger.value]
+                        if data_type != "trending":
+                            options = generate_options(dut)
+                        disabled = C.STYLE_DISPLAY
+                    except KeyError:
+                        options = list()
+                        disabled = C.STYLE_DONT_DISPLAY
+                    if data_type == "trending":
+                        ctrl_panel.set({
+                            "dut-val": trigger.value,
+                            "release-opt": list(),
+                            "release-dis": C.STYLE_DONT_DISPLAY,
+                            "release-val": str(),
+                            "help-dis": disabled,
+                            "help-val": "<testbed> <nic> <driver> " + \
+                                "<framesize> <cores> <test>",
+                            "search-dis": disabled,
+                            "search-val": str()
+                        })
+                    else:
+                        ctrl_panel.set({
+                            "dut-val": trigger.value,
+                            "release-opt": options,
+                            "release-dis": disabled,
+                            "release-val": str(),
+                            "help-dis": C.STYLE_DONT_DISPLAY,
+                            "help-val": str(),
+                            "search-dis": C.STYLE_DONT_DISPLAY,
+                            "search-val": str()
+                        })
+                elif trigger.idx == "release":
+                    ctrl_panel.set({
+                        "release-val": trigger.value,
+                        "help-dis": C.STYLE_DISPLAY,
+                        "help-val": "<DUT version> <testbed> <nic> " + \
+                            "<driver> <framesize> <core> <test>",
+                        "search-dis": C.STYLE_DISPLAY,
+                        "search-val": str()
+                    })
+                elif trigger.idx == "search":
+                    ctrl_panel.set({"search-val": trigger.value})
+                    selection = {
+                        "datatype": ctrl_panel.get("datatype-val"),
+                        "dut": ctrl_panel.get("dut-val"),
+                        "release": ctrl_panel.get("release-val"),
+                        "help": ctrl_panel.get("help-val"),
+                        "regexp":  ctrl_panel.get("search-val"),
+                    }
+                    on_draw = True
+            elif trigger.type == "table" and trigger.idx == "search":
+                filtered_data = filter_table_data(
+                    store_table_data,
+                    trigger.value
+                )
+                table_data = [filtered_data, ]
+
+            if on_draw:
+                table = search_table(data=self._data, selection=selection)
+                plotting_area = Layout._get_plotting_area(
+                    table,
+                    gen_new_url(parsed_url, {"selection": selection})
+                )
+                store_table_data = table.to_dict("records")
+                filtered_data = store_table_data
+                if table_data:
+                    table_data = [store_table_data, ]
+            else:
+                plotting_area = no_update
+
+            store["control-panel"] = ctrl_panel.panel
+            store["selection"] = selection
+            ret_val = [
+                store,
+                store_table_data,
+                filtered_data,
+                plotting_area,
+                table_data
+            ]
+            ret_val.extend(ctrl_panel.values)
+
+            return ret_val
+
+        @app.callback(
+            Output("offcanvas-details", "is_open"),
+            Output("offcanvas-details", "children"),
+            State("store", "data"),
+            State("store-filtered-table-data", "data"),
+            Input({"type": "table", "index": ALL}, "active_cell"),
+            prevent_initial_call=True
+        )
+        def show_test_data(store, table, *_):
+            """Show offcanvas with graphs and tables based on selected test(s).
+            """
+
+            trigger = Trigger(callback_context.triggered)
+            if not trigger.value:
+                raise PreventUpdate
+
+            try:
+                row = pd.DataFrame.from_records(table).\
+                    iloc[[trigger.value["row"]]]
+                datatype = store["selection"]["datatype"]
+                dut = store["selection"]["dut"]
+                rls = store["selection"]["release"]
+                tb = row["Test Bed"].iloc[0]
+                nic = row["NIC"].iloc[0]
+                driver = row['Driver'].iloc[0]
+                test_name = row['Test'].iloc[0]
+                dutver = str()
+            except(KeyError, IndexError, AttributeError, ValueError):
+                raise PreventUpdate
+
+            data = self._data[datatype]
+            if datatype == "trending":
+                df = pd.DataFrame(data.loc[data["dut_type"] == dut])
+            else:
+                dutver = row["DUT Version"].iloc[0]
+                df = pd.DataFrame(data.loc[(
+                    (data["dut_type"] == dut) &
+                    (data["dut_version"] == dutver) &
+                    (data["release"] == rls)
+                )])
+
+            df = df[df.full_id.str.contains(
+                f".*{tb}.*{nic}.*{test_name}",
+                regex=True
+            )]
+
+            if datatype in ("trending", "iterative"):
+                l_test_id = df["test_id"].iloc[0].split(".")
+                if dut == "dpdk":
+                    area = "dpdk"
+                else:
+                    area = ".".join(l_test_id[3:-2])
+                for drv in C.DRIVERS:
+                    if drv in test_name:
+                        test = test_name.replace(f"{drv}-", "")
+                        break
+                else:
+                    test = test_name
+                l_test = test.split("-")
+                testtype = l_test[-1]
+                if testtype == "ndrpdr":
+                    testtype = ["ndr", "pdr"]
+                else:
+                    testtype = [testtype, ]
+                core = l_test[1] if l_test[1] else "8c"
+                test = "-".join(l_test[2: -1])
+                test_id = f"{tb}-{nic}-{driver}-{core}-{l_test[0]}-{test}"
+                title = dbc.Row(
+                    class_name="g-0 p-0",
+                    children=dbc.Alert(test_id, color="info"),
+                )
+                selected = list()
+                indexes = ("tput", "bandwidth", "lat")
+                if datatype == "trending":
+                    for ttype in testtype:
+                        selected.append({
+                            "id": f"{dut}-{test_id}-{ttype}",
+                            "dut": dut,
+                            "phy": f"{tb}-{nic}-{driver}",
+                            "area": area,
+                            "test": test,
+                            "framesize": l_test[0],
+                            "core": core,
+                            "testtype": ttype
+                        })
+                    graphs = graph_trending(df, selected, self._graph_layout)
+                    labels = ("Throughput", "Bandwidth", "Latency")
+                    tabs = list()
+                    for graph, label, idx in zip(graphs, labels, indexes):
+                        if graph:
+                            tabs.append(dbc.Tab(
+                                children=dcc.Graph(
+                                    figure=graph,
+                                    id={"type": "graph-trend", "index": idx},
+                                ),
+                                label=label
+                            ))
+                    if tabs:
+                        ret_val = [
+                            title,
+                            dbc.Row(dbc.Tabs(tabs), class_name="g-0 p-0")
+                        ]
+                    else:
+                        ret_val = [
+                            title,
+                            dbc.Row("No data.", class_name="g-0 p-0")
+                        ]
+
+                else:  # Iterative
+                    for ttype in testtype:
+                        selected.append({
+                            "id": f"{test_id}-{ttype}",
+                            "rls": rls,
+                            "dut": dut,
+                            "dutver": dutver,
+                            "phy": f"{tb}-{nic}-{driver}",
+                            "area": area,
+                            "test": test,
+                            "framesize": l_test[0],
+                            "core": core,
+                            "testtype": ttype
+                        })
+                    graphs = graph_iterative(df, selected, self._graph_layout)
+                    cols = list()
+                    for graph, idx in zip(graphs, indexes):
+                        if graph:
+                            cols.append(dbc.Col(dcc.Graph(
+                                figure=graph,
+                                id={"type": "graph-iter", "index": idx},
+                            )))
+                    if not cols:
+                        cols="No data."
+                    ret_val = [
+                        title,
+                        dbc.Row(class_name="g-0 p-0", children=cols)
+                    ]
+
+            elif datatype == "coverage":
+                ret_val = coverage_tables(
+                    data=df,
+                    selected={
+                        "rls": rls,
+                        "dut": dut,
+                        "dutver": dutver,
+                        "phy": f"{tb}-{nic}-{driver}",
+                        "area": ".*",
+                    },
+                    start_collapsed=False
+                )
+            else:
+                raise PreventUpdate
+
+            return True, ret_val
+
+        @app.callback(
+            Output("metadata-tput-lat", "children"),
+            Output("metadata-hdrh-graph", "children"),
+            Output("offcanvas-metadata", "is_open"),
+            Input({"type": "graph-trend", "index": ALL}, "clickData"),
+            Input({"type": "graph-iter", "index": ALL}, "clickData"),
+            prevent_initial_call=True
+        )
+        def _show_metadata_from_trend_graph(
+                trend_data: dict,
+                iter_data: dict
+            ) -> tuple:
+            """Generates the data for the offcanvas displayed when a particular
+            point in a graph is clicked on.
+            """
+
+            trigger = Trigger(callback_context.triggered)
+            if not trigger.value:
+                raise PreventUpdate
+
+            if trigger.type == "graph-trend":
+                return show_trending_graph_data(
+                    trigger, trend_data, self._graph_layout)
+            elif trigger.type == "graph-iter":
+                return show_iterative_graph_data(
+                    trigger, iter_data, self._graph_layout)
+            else:
+                raise PreventUpdate
+
+        @app.callback(
+            Output("plot-mod-url", "is_open"),
+            Input("plot-btn-url", "n_clicks"),
+            State("plot-mod-url", "is_open")
+        )
+        def toggle_plot_mod_url(n, is_open):
+            """Toggle the modal window with url.
+            """
+            if n:
+                return not is_open
+            return is_open
+
+        @app.callback(
+            Output("download-data", "data"),
+            State("store-filtered-table-data", "data"),
+            Input("plot-btn-download", "n_clicks"),
+            prevent_initial_call=True
+        )
+        def _download_search_data(selection, _):
+            """Download the data.
+
+            :param selection: Selected data in table format (records).
+            :type selection: dict
+            :returns: dict of data frame content (base64 encoded) and meta data
+                used by the Download component.
+            :rtype: dict
+            """
+
+            if not selection:
+                raise PreventUpdate
+
+            return dcc.send_data_frame(
+                pd.DataFrame.from_records(selection).to_csv,
+                C.SEARCH_DOWNLOAD_FILE_NAME
+            )
+
+        @app.callback(
+            Output("offcanvas-documentation", "is_open"),
+            Input("btn-documentation", "n_clicks"),
+            State("offcanvas-documentation", "is_open")
+        )
+        def toggle_offcanvas_documentation(n_clicks, is_open):
+            if n_clicks:
+                return not is_open
+            return is_open
diff --git a/csit.infra.dash/app/cdash/search/layout.yaml b/csit.infra.dash/app/cdash/search/layout.yaml
new file mode 100644 (file)
index 0000000..7d86e53
--- /dev/null
@@ -0,0 +1,276 @@
+plot-throughput:
+  xaxis:
+    title: "Test Cases [Index]"
+    autorange: True
+    fixedrange: False
+    gridcolor: "rgb(230, 230, 230)"
+    linecolor: "rgb(220, 220, 220)"
+    linewidth: 1
+    showgrid: True
+    showline: True
+    showticklabels: True
+    tickcolor: "rgb(220, 220, 220)"
+    tickmode: "array"
+    zeroline: True
+  yaxis:
+    title: "Throughput [pps|cps|rps|bps]"
+    gridcolor: "rgb(230, 230, 230)"
+    hoverformat: ".3s"
+    tickformat: ".3s"
+    linecolor: "rgb(220, 220, 220)"
+    linewidth: 1
+    showgrid: True
+    showline: True
+    showticklabels: True
+    tickcolor: "rgb(220, 220, 220)"
+    zeroline: True
+    range: [0, 100]
+  autosize: True
+  margin:
+    t: 50
+    b: 0
+    l: 80
+    r: 20
+  showlegend: False
+  height: 850
+  paper_bgcolor: "#fff"
+  plot_bgcolor: "#fff"
+  hoverlabel:
+    namelength: -1
+
+plot-bandwidth:
+  xaxis:
+    title: "Test Cases [Index]"
+    autorange: True
+    fixedrange: False
+    gridcolor: "rgb(230, 230, 230)"
+    linecolor: "rgb(220, 220, 220)"
+    linewidth: 1
+    showgrid: True
+    showline: True
+    showticklabels: True
+    tickcolor: "rgb(220, 220, 220)"
+    tickmode: "array"
+    zeroline: True
+  yaxis:
+    title: "Bandwidth [bps]"
+    gridcolor: "rgb(230, 230, 230)"
+    hoverformat: ".3s"
+    tickformat: ".3s"
+    linecolor: "rgb(220, 220, 220)"
+    linewidth: 1
+    showgrid: True
+    showline: True
+    showticklabels: True
+    tickcolor: "rgb(220, 220, 220)"
+    zeroline: True
+    range: [0, 200]
+  autosize: True
+  margin:
+    t: 50
+    b: 0
+    l: 80
+    r: 20
+  showlegend: False
+  height: 850
+  paper_bgcolor: "#fff"
+  plot_bgcolor: "#fff"
+  hoverlabel:
+    namelength: -1
+
+plot-latency:
+  xaxis:
+    title: "Test Cases [Index]"
+    autorange: True
+    fixedrange: False
+    gridcolor: "rgb(230, 230, 230)"
+    linecolor: "rgb(220, 220, 220)"
+    linewidth: 1
+    showgrid: True
+    showline: True
+    showticklabels: True
+    tickcolor: "rgb(220, 220, 220)"
+    tickmode: "array"
+    zeroline: True
+  yaxis:
+    title: "Average Latency at 50% PDR [us]"
+    gridcolor: "rgb(230, 230, 230)"
+    hoverformat: ".3s"
+    tickformat: ".3s"
+    linecolor: "rgb(220, 220, 220)"
+    linewidth: 1
+    showgrid: True
+    showline: True
+    showticklabels: True
+    tickcolor: "rgb(220, 220, 220)"
+    zeroline: True
+    range: [0, 200]
+  autosize: True
+  margin:
+    t: 50
+    b: 0
+    l: 80
+    r: 20
+  showlegend: False
+  height: 850
+  paper_bgcolor: "#fff"
+  plot_bgcolor: "#fff"
+  hoverlabel:
+    namelength: -1
+
+plot-trending-tput:
+  autosize: True
+  showlegend: False
+  yaxis:
+    showticklabels: True
+    tickformat: ".3s"
+    title: "Throughput [pps|cps|rps|bps]"
+    hoverformat: ".5s"
+    gridcolor: "rgb(238, 238, 238)"
+    linecolor: "rgb(238, 238, 238)"
+    showline: True
+    zeroline: False
+    tickcolor: "rgb(238, 238, 238)"
+    linewidth: 1
+    showgrid: True
+  xaxis:
+    title: 'Date [MMDD]'
+    type: "date"
+    autorange: True
+    fixedrange: False
+    showgrid: True
+    gridcolor: "rgb(238, 238, 238)"
+    showline: True
+    linecolor: "rgb(238, 238, 238)"
+    zeroline: False
+    linewidth: 1
+    showticklabels: True
+    tickcolor: "rgb(238, 238, 238)"
+    tickmode: "auto"
+    tickformat: "%m%d"
+  margin:
+    r: 20
+    b: 0
+    t: 5
+    l: 70
+  paper_bgcolor: "#fff"
+  plot_bgcolor: "#fff"
+  hoverlabel:
+    namelength: -1
+
+plot-trending-bandwidth:
+  autosize: True
+  showlegend: False
+  yaxis:
+    showticklabels: True
+    tickformat: ".3s"
+    title: "Bandwidth [bps]"
+    hoverformat: ".5s"
+    gridcolor: "rgb(238, 238, 238)"
+    linecolor: "rgb(238, 238, 238)"
+    showline: True
+    zeroline: False
+    tickcolor: "rgb(238, 238, 238)"
+    linewidth: 1
+    showgrid: True
+  xaxis:
+    title: 'Date [MMDD]'
+    type: "date"
+    autorange: True
+    fixedrange: False
+    showgrid: True
+    gridcolor: "rgb(238, 238, 238)"
+    showline: True
+    linecolor: "rgb(238, 238, 238)"
+    zeroline: False
+    linewidth: 1
+    showticklabels: True
+    tickcolor: "rgb(238, 238, 238)"
+    tickmode: "auto"
+    tickformat: "%m%d"
+  margin:
+    r: 20
+    b: 0
+    t: 5
+    l: 70
+  paper_bgcolor: "#fff"
+  plot_bgcolor: "#fff"
+  hoverlabel:
+    namelength: -1
+
+plot-trending-lat:
+  autosize: True
+  showlegend: False
+  yaxis:
+    showticklabels: True
+    tickformat: ".3s"
+    title: "Average Latency at 50% PDR [us]"
+    hoverformat: ".5s"
+    gridcolor: "rgb(238, 238, 238)"
+    linecolor: "rgb(238, 238, 238)"
+    showline: True
+    zeroline: False
+    tickcolor: "rgb(238, 238, 238)"
+    linewidth: 1
+    showgrid: True
+  xaxis:
+    title: 'Date [MMDD]'
+    type: "date"
+    autorange: True
+    fixedrange: False
+    showgrid: True
+    gridcolor: "rgb(238, 238, 238)"
+    showline: True
+    linecolor: "rgb(238, 238, 238)"
+    zeroline: False
+    linewidth: 1
+    showticklabels: True
+    tickcolor: "rgb(238, 238, 238)"
+    tickmode: "auto"
+    tickformat: "%m%d"
+  margin:
+    r: 20
+    b: 0
+    t: 5
+    l: 70
+  paper_bgcolor: "#fff"
+  plot_bgcolor: "#fff"
+  hoverlabel:
+    namelength: -1
+
+plot-hdrh-latency:
+  showlegend: True
+  legend:
+    traceorder: "normal"
+    orientation: "h"
+    xanchor: "left"
+    yanchor: "top"
+    x: 0
+    y: -0.25
+    bgcolor: "rgba(255, 255, 255, 0)"
+    bordercolor: "rgba(255, 255, 255, 0)"
+  xaxis:
+    type: "log"
+    title: "Percentile [%]"
+    autorange: True
+    gridcolor: "rgb(230, 230, 230)"
+    linecolor: "rgb(220, 220, 220)"
+    linewidth: 1
+    showgrid: True
+    showline: True
+    showticklabels: True
+    tickcolor: "rgb(220, 220, 220)"
+    tickvals: [1, 2, 1e1, 20, 1e2, 1e3, 1e4, 1e5, 1e6]
+    ticktext: [0, 50, 90, 95, 99, 99.9, 99.99, 99.999, 99.9999]
+  yaxis:
+    title: "One-Way Latency per Direction [us]"
+    gridcolor: "rgb(230, 230, 230)"
+    linecolor: "rgb(220, 220, 220)"
+    linewidth: 1
+    showgrid: True
+    showline: True
+    showticklabels: True
+    tickcolor: "rgb(220, 220, 220)"
+  autosize: True
+  paper_bgcolor: "white"
+  plot_bgcolor: "white"
diff --git a/csit.infra.dash/app/cdash/search/search.py b/csit.infra.dash/app/cdash/search/search.py
new file mode 100644 (file)
index 0000000..0ecdcb7
--- /dev/null
@@ -0,0 +1,52 @@
+# Copyright (c) 2024 Cisco and/or its affiliates.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Instantiate the Search Dash application.
+"""
+
+import dash
+
+from ..utils.constants import Constants as C
+from .layout import Layout
+
+
+def init_search(
+        server,
+        data: tuple
+    ) -> dash.Dash:
+    """Create a Plotly Dash dashboard.
+
+    :param server: Flask server.
+    :type server: Flask
+    :returns: Dash app server.
+    :rtype: Dash
+    """
+
+    dash_app = dash.Dash(
+        server=server,
+        routes_pathname_prefix=C.SEARCH_ROUTES_PATHNAME_PREFIX,
+        external_stylesheets=C.EXTERNAL_STYLESHEETS,
+        title=C.SEARCH_TITLE
+    )
+
+    layout = Layout(
+        app=dash_app,
+        data=data,
+        html_layout_file=C.HTML_LAYOUT_FILE,
+        graph_layout_file=C.SEARCH_GRAPH_LAYOUT_FILE,
+        tooltip_file=C.TOOLTIP_FILE
+    )
+    dash_app.index_string = layout.html_layout
+    dash_app.layout = layout.add_content()
+
+    return dash_app.server
diff --git a/csit.infra.dash/app/cdash/search/tables.py b/csit.infra.dash/app/cdash/search/tables.py
new file mode 100644 (file)
index 0000000..a5ffd76
--- /dev/null
@@ -0,0 +1,123 @@
+# Copyright (c) 2024 Cisco and/or its affiliates.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""The search data tables.
+"""
+
+
+import pandas as pd
+
+from ..utils.constants import Constants as C
+
+
+def select_search_data(data: pd.DataFrame, selection: list) -> pd.DataFrame:
+    """Return the searched data based on the user's "selection".
+
+    :param data: Input data to be searched through.
+    :param selection: User selection.
+    :type data: pandas.DataFrame
+    :type selection: list[dict]
+    :returns: A dataframe with selected tests.
+    :trype: pandas.DataFrame
+    """
+
+    sel_data = data[selection["datatype"]]
+
+    if selection["datatype"] == "trending":
+        df = pd.DataFrame(sel_data.loc[
+            sel_data["dut_type"] == selection["dut"]
+        ])
+    else:
+        df = pd.DataFrame(sel_data.loc[(
+            (sel_data["dut_type"] == selection["dut"]) &
+            (sel_data["release"] == selection["release"])
+        )])
+    try:
+        df = df[
+            df.full_id.str.contains(
+                selection["regexp"].replace(" ", ".*"),
+                regex=True
+            )
+        ]
+    except Exception:
+        return pd.DataFrame()
+
+    return df
+
+
+def search_table(data: pd.DataFrame, selection: list) -> pd.DataFrame:
+    """Generate a table listing tests based on user's selection.
+
+    :param data: Input data (all tests).
+    :param selection: User selection.
+    :type data: pandas.DataFrame
+    :type selection: list[dict]
+    :returns: A dataframe with selected tests/
+    :rtype: pandas.DataFrame
+    """
+
+    sel = select_search_data(data, selection)
+    if sel.empty:
+        return pd.DataFrame()
+
+    l_tb, l_nic, l_drv, l_test, = list(), list(), list(), list()
+    if selection["datatype"] == "trending":
+        cols = ["job", "test_id"]
+    else:
+        l_dutver = list()
+        cols = ["job", "test_id", "dut_version"]
+    for _, row in sel[cols].drop_duplicates().iterrows():
+        l_id = row["test_id"].split(".")
+        suite = l_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
+            replace("2n-", "")
+        l_tb.append("-".join(row["job"].split("-")[-2:]))
+        l_nic.append(suite.split("-")[0])
+        if selection["datatype"] != "trending":
+            l_dutver.append(row["dut_version"])
+        for driver in C.DRIVERS:
+            if driver in suite:
+                l_drv.append(driver)
+                break
+        else:
+            l_drv.append("dpdk")
+        l_test.append(l_id[-1])
+
+    if selection["datatype"] == "trending":
+        selected = pd.DataFrame.from_dict({
+            "Test Bed": l_tb,
+            "NIC": l_nic,
+            "Driver": l_drv,
+            "Test": l_test
+        })
+
+        selected.sort_values(
+            by=["Test Bed", "NIC", "Driver", "Test"],
+            ascending=True,
+            inplace=True
+        )
+    else:
+        selected = pd.DataFrame.from_dict({
+            "DUT Version": l_dutver,
+            "Test Bed": l_tb,
+            "NIC": l_nic,
+            "Driver": l_drv,
+            "Test": l_test
+        })
+
+        selected.sort_values(
+            by=["DUT Version", "Test Bed", "NIC", "Driver", "Test"],
+            ascending=True,
+            inplace=True
+        )
+
+    return selected
index f0d52c2..c6a5f63 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 2223848..4b25396 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 753eb37..56b24e0 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -29,7 +29,7 @@ from yaml import load, FullLoader, YAMLError
 from ..utils.constants import Constants as C
 from ..utils.control_panel import ControlPanel
 from ..utils.utils import show_tooltip, gen_new_url, get_ttypes, get_cadences, \
 from ..utils.constants import Constants as C
 from ..utils.control_panel import ControlPanel
 from ..utils.utils import show_tooltip, gen_new_url, get_ttypes, get_cadences, \
-    get_test_beds, get_job, generate_options, set_job_params
+    get_test_beds, get_job, generate_options, set_job_params, navbar_trending
 from ..utils.url_processing import url_decode
 from .graphs import graph_statistics, select_data
 
 from ..utils.url_processing import url_decode
 from .graphs import graph_statistics, select_data
 
@@ -233,9 +233,7 @@ class Layout:
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
-                        children=[
-                            self._add_navbar()
-                        ]
+                        children=[navbar_trending((False, False, True, False))]
                     ),
                     dbc.Spinner(
                         dbc.Offcanvas(
                     ),
                     dbc.Spinner(
                         dbc.Offcanvas(
@@ -284,43 +282,6 @@ class Layout:
                 ]
             )
 
                 ]
             )
 
-    def _add_navbar(self):
-        """Add nav element with navigation panel. It is placed on the top.
-
-        :returns: Navigation bar.
-        :rtype: dbc.NavbarSimple
-        """
-        return dbc.NavbarSimple(
-            id="navbarsimple-main",
-            children=[
-                dbc.NavItem(dbc.NavLink(
-                    C.TREND_TITLE,
-                    external_link=True,
-                    href="/trending"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    C.NEWS_TITLE,
-                    external_link=True,
-                    href="/news"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    C.STATS_TITLE,
-                    active=True,
-                    external_link=True,
-                    href="/stats"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Documentation",
-                    id="btn-documentation",
-                ))
-            ],
-            brand=C.BRAND,
-            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.
 
     def _add_ctrl_col(self) -> dbc.Col:
         """Add column with controls. It is placed on the left side.
 
index fdeef8b..0217a6e 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 7250480..7b0dadc 100644 (file)
           {{ news_title }}
         </a>
       </p>
           {{ news_title }}
         </a>
       </p>
+      <p>
+        <a href="/search/" class="btn btn-primary fw-bold w-25">
+          {{ search_title }}
+        </a>
+      </p>
       <p>
         <a href="/cdocs/" class="btn btn-primary fw-bold w-25">
           Documentation
       <p>
         <a href="/cdocs/" class="btn btn-primary fw-bold w-25">
           Documentation
index f0d52c2..c6a5f63 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 57fc165..ede3a06 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -71,7 +71,7 @@ def graph_trending(
         data: pd.DataFrame,
         sel: dict,
         layout: dict,
         data: pd.DataFrame,
         sel: dict,
         layout: dict,
-        normalize: bool
+        normalize: bool=False
     ) -> tuple:
     """Generate the trending graph(s) - MRR, NDR, PDR and for PDR also Latences
     (result_latency_forward_pdr_50_avg).
     ) -> tuple:
     """Generate the trending graph(s) - MRR, NDR, PDR and for PDR also Latences
     (result_latency_forward_pdr_50_avg).
index 66aa1d1..f6f96d7 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -34,7 +34,8 @@ from ..utils.control_panel import ControlPanel
 from ..utils.trigger import Trigger
 from ..utils.telemetry_data import TelemetryData
 from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
 from ..utils.trigger import Trigger
 from ..utils.telemetry_data import TelemetryData
 from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
-    generate_options, get_list_group_items, graph_hdrh_latency
+    generate_options, get_list_group_items, navbar_trending, \
+    show_trending_graph_data
 from ..utils.url_processing import url_decode
 from .graphs import graph_trending, select_trending_data, graph_tm_trending
 
 from ..utils.url_processing import url_decode
 from .graphs import graph_trending, select_trending_data, graph_tm_trending
 
@@ -244,9 +245,7 @@ class Layout:
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
-                        children=[
-                            self._add_navbar()
-                        ]
+                        children=[navbar_trending((True, False, False, False))]
                     ),
                     dbc.Row(
                         id="row-main",
                     ),
                     dbc.Row(
                         id="row-main",
@@ -290,43 +289,6 @@ class Layout:
                 id="div-main-error"
             )
 
                 id="div-main-error"
             )
 
-    def _add_navbar(self):
-        """Add nav element with navigation panel. It is placed on the top.
-
-        :returns: Navigation bar.
-        :rtype: dbc.NavbarSimple
-        """
-        return dbc.NavbarSimple(
-            children=[
-                dbc.NavItem(dbc.NavLink(
-                    C.TREND_TITLE,
-                    active=True,
-                    external_link=True,
-                    href="/trending"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    C.NEWS_TITLE,
-                    external_link=True,
-                    href="/news"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    C.STATS_TITLE,
-                    external_link=True,
-                    href="/stats"
-                )),
-                dbc.NavItem(dbc.NavLink(
-                    "Documentation",
-                    id="btn-documentation",
-                ))
-            ],
-            id="navbarsimple-main",
-            brand=C.BRAND,
-            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.
 
     def _add_ctrl_col(self) -> dbc.Col:
         """Add column with controls. It is placed on the left side.
 
@@ -1692,91 +1654,11 @@ class Layout:
             """
 
             trigger = Trigger(callback_context.triggered)
             """
 
             trigger = Trigger(callback_context.triggered)
-
-            try:
-                if trigger.idx == "tput":
-                    idx = 0
-                elif trigger.idx == "bandwidth":
-                    idx = 1
-                elif trigger.idx == "lat":
-                    idx = 2
-                else:
-                    raise PreventUpdate
-                graph_data = graph_data[idx]["points"][0]
-            except (IndexError, KeyError, ValueError, TypeError):
-                raise PreventUpdate
-
-            metadata = no_update
-            graph = list()
-
-            list_group_items = list()
-            for itm in graph_data.get("text", None).split("<br>"):
-                if not itm:
-                    continue
-                lst_itm = itm.split(": ")
-                if lst_itm[0] == "csit-ref":
-                    list_group_item = dbc.ListGroupItem([
-                        dbc.Badge(lst_itm[0]),
-                        html.A(
-                            lst_itm[1],
-                            href=f"{C.URL_JENKINS}{lst_itm[1]}",
-                            target="_blank"
-                        )
-                    ])
-                else:
-                    list_group_item = dbc.ListGroupItem([
-                        dbc.Badge(lst_itm[0]),
-                        lst_itm[1]
-                    ])
-                list_group_items.append(list_group_item)
-
-            if trigger.idx == "tput":
-                title = "Throughput"
-            elif trigger.idx == "bandwidth":
-                title = "Bandwidth"
-            elif trigger.idx == "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(
-                                dcc.Graph(
-                                    id="hdrh-latency-graph",
-                                    figure=graph_hdrh_latency(
-                                        hdrh_data, self._graph_layout
-                                    )
-                                )
-                            )
-                        ])
-                    ]
-            else:
+            if not trigger.value:
                 raise PreventUpdate
                 raise PreventUpdate
-
-            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(
-                            dbc.ListGroup(list_group_items, flush=True),
-                            id="tput-lat-metadata",
-                            class_name="p-0",
-                        )
-                    ]
-                )
-            ]
-
-            return metadata, graph, True
+            
+            return show_trending_graph_data(
+                    trigger, graph_data, self._graph_layout)
 
         @app.callback(
             Output("download-trending-data", "data"),
 
         @app.callback(
             Output("download-trending-data", "data"),
index a9dfbc1..257e3de 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index f0d52c2..c6a5f63 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 9a7b232..3deece2 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 4ffd7c1..c86f4d5 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -97,6 +97,12 @@ class Constants:
     # The element is enabled and visible.
     STYLE_ENABLED = {"visibility": "visible"}
 
     # The element is enabled and visible.
     STYLE_ENABLED = {"visibility": "visible"}
 
+    # The element is not displayed.
+    STYLE_DONT_DISPLAY = {"display": "none"}
+
+    # The element is displaed.
+    STYLE_DISPLAY = {"display": "flex"}
+
     # Checklist "All" is disabled.
     CL_ALL_DISABLED = [
         {
     # Checklist "All" is disabled.
     CL_ALL_DISABLED = [
         {
@@ -403,3 +409,18 @@ class Constants:
     COVERAGE_DOWNLOAD_FILE_NAME = "coverage_data.csv"
 
     ############################################################################
     COVERAGE_DOWNLOAD_FILE_NAME = "coverage_data.csv"
 
     ############################################################################
+    # Search tests.
+
+    # The title.
+    SEARCH_TITLE = "Search Tests"
+
+    # The pathname prefix for the application.
+    SEARCH_ROUTES_PATHNAME_PREFIX = "/search/"
+
+    # Layout of plot.ly graphs.
+    SEARCH_GRAPH_LAYOUT_FILE = "cdash/search/layout.yaml"
+
+    # Default name of downloaded file with selected data.
+    SEARCH_DOWNLOAD_FILE_NAME = "search_data.csv"
+
+    ############################################################################
index a81495e..3da44e3 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 8018796..9975874 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index ac303b6..da0768b 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index c90c54c..c436ebc 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index 29bee3d..3d2866f 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
@@ -22,11 +22,12 @@ import hdrh.histogram
 import hdrh.codec
 
 from math import sqrt
 import hdrh.codec
 
 from math import sqrt
-from dash import dcc
+from dash import dcc, no_update, html
 from datetime import datetime
 
 from ..utils.constants import Constants as C
 from ..utils.url_processing import url_encode
 from datetime import datetime
 
 from ..utils.constants import Constants as C
 from ..utils.url_processing import url_encode
+from ..utils.trigger import Trigger
 
 
 def get_color(idx: int) -> str:
 
 
 def get_color(idx: int) -> str:
@@ -468,3 +469,394 @@ def graph_hdrh_latency(data: dict, layout: dict) -> go.Figure:
             fig.update_layout(layout_hdrh)
 
     return fig
             fig.update_layout(layout_hdrh)
 
     return fig
+
+
+def navbar_trending(active: tuple):
+    """Add nav element with navigation panel. It is placed on the top.
+
+    :param active: Tuple of boolean values defining the active items in the
+        navbar. True == active
+    :type active: tuple
+    :returns: Navigation bar.
+    :rtype: dbc.NavbarSimple
+    """
+    return dbc.NavbarSimple(
+        children=[
+            dbc.NavItem(dbc.NavLink(
+                C.TREND_TITLE,
+                active=active[0],
+                external_link=True,
+                href="/trending"
+            )),
+            dbc.NavItem(dbc.NavLink(
+                C.NEWS_TITLE,
+                active=active[1],
+                external_link=True,
+                href="/news"
+            )),
+            dbc.NavItem(dbc.NavLink(
+                C.STATS_TITLE,
+                active=active[2],
+                external_link=True,
+                href="/stats"
+            )),
+            dbc.NavItem(dbc.NavLink(
+                C.SEARCH_TITLE,
+                active=active[3],
+                external_link=True,
+                href="/search"
+            )),
+            dbc.NavItem(dbc.NavLink(
+                "Documentation",
+                id="btn-documentation",
+            ))
+        ],
+        id="navbarsimple-main",
+        brand=C.BRAND,
+        brand_href="/",
+        brand_external_link=True,
+        class_name="p-2",
+        fluid=True
+    )
+
+
+def navbar_report(active: tuple):
+    """Add nav element with navigation panel. It is placed on the top.
+
+    :param active: Tuple of boolean values defining the active items in the
+        navbar. True == active
+    :type active: tuple
+    :returns: Navigation bar.
+    :rtype: dbc.NavbarSimple
+    """
+    return dbc.NavbarSimple(
+        id="navbarsimple-main",
+        children=[
+            dbc.NavItem(dbc.NavLink(
+                C.REPORT_TITLE,
+                active=active[0],
+                external_link=True,
+                href="/report"
+            )),
+            dbc.NavItem(dbc.NavLink(
+                "Comparisons",
+                active=active[1],
+                external_link=True,
+                href="/comparisons"
+            )),
+            dbc.NavItem(dbc.NavLink(
+                "Coverage Data",
+                active=active[2],
+                external_link=True,
+                href="/coverage"
+            )),
+            dbc.NavItem(dbc.NavLink(
+                C.SEARCH_TITLE,
+                active=active[3],
+                external_link=True,
+                href="/search"
+            )),
+            dbc.NavItem(dbc.NavLink(
+                "Documentation",
+                id="btn-documentation",
+            ))
+        ],
+        brand=C.BRAND,
+        brand_href="/",
+        brand_external_link=True,
+        class_name="p-2",
+        fluid=True
+    )
+
+
+def filter_table_data(
+        store_table_data: list,
+        table_filter: str
+    ) -> list:
+    """Filter table data using user specified filter.
+
+    :param store_table_data: Table data represented as a list of records.
+    :param table_filter: User specified filter.
+    :type store_table_data: list
+    :type table_filter: str
+    :returns: A new table created by filtering of table data represented as
+        a list of records.
+    :rtype: list
+    """
+
+    # Checks:
+    if not any((table_filter, store_table_data, )):
+        return store_table_data
+
+    def _split_filter_part(filter_part: str) -> tuple:
+        """Split a part of filter into column name, operator and value.
+        A "part of filter" is a sting berween "&&" operator.
+
+        :param filter_part: A part of filter.
+        :type filter_part: str
+        :returns: Column name, operator, value
+        :rtype: tuple[str, str, str|float]
+        """
+        for operator_type in C.OPERATORS:
+            for operator in operator_type:
+                if operator in filter_part:
+                    name_p, val_p = filter_part.split(operator, 1)
+                    name = name_p[name_p.find("{") + 1 : name_p.rfind("}")]
+                    val_p = val_p.strip()
+                    if (val_p[0] == val_p[-1] and val_p[0] in ("'", '"', '`')):
+                        value = val_p[1:-1].replace("\\" + val_p[0], val_p[0])
+                    else:
+                        try:
+                            value = float(val_p)
+                        except ValueError:
+                            value = val_p
+
+                    return name, operator_type[0].strip(), value
+        return (None, None, None)
+
+    df = pd.DataFrame.from_records(store_table_data)
+    for filter_part in table_filter.split(" && "):
+        col_name, operator, filter_value = _split_filter_part(filter_part)
+        if operator == "contains":
+            df = df.loc[df[col_name].str.contains(filter_value, regex=True)]
+        elif operator in ("eq", "ne", "lt", "le", "gt", "ge"):
+            # These operators match pandas series operator method names.
+            df = df.loc[getattr(df[col_name], operator)(filter_value)]
+        elif operator == "datestartswith":
+            # This is a simplification of the front-end filtering logic,
+            # only works with complete fields in standard format.
+            # Currently not used in comparison tables.
+            df = df.loc[df[col_name].str.startswith(filter_value)]
+
+    return df.to_dict("records")
+
+
+def show_trending_graph_data(
+        trigger: Trigger,
+        data: dict,
+        graph_layout: dict
+    ) -> tuple:
+    """Generates the data for the offcanvas displayed when a particular point in
+    a trending graph (daily data) is clicked on.
+
+    :param trigger: The information from trigger when the data point is clicked
+        on.
+    :param graph: The data from the clicked point in the graph.
+    :param graph_layout: The layout of the HDRH latency graph.
+    :type trigger: Trigger
+    :type graph: dict
+    :type graph_layout: dict
+    :returns: The data to be displayed on the offcanvas and the information to
+        show the offcanvas.
+    :rtype: tuple(list, list, bool)
+    """
+
+    if trigger.idx == "tput":
+        idx = 0
+    elif trigger.idx == "bandwidth":
+        idx = 1
+    elif trigger.idx == "lat":
+        idx = len(data) - 1
+    else:
+        return list(), list(), False
+    try:
+        data = data[idx]["points"][0]
+    except (IndexError, KeyError, ValueError, TypeError):
+        return list(), list(), False
+
+    metadata = no_update
+    graph = list()
+
+    list_group_items = list()
+    for itm in data.get("text", None).split("<br>"):
+        if not itm:
+            continue
+        lst_itm = itm.split(": ")
+        if lst_itm[0] == "csit-ref":
+            list_group_item = dbc.ListGroupItem([
+                dbc.Badge(lst_itm[0]),
+                html.A(
+                    lst_itm[1],
+                    href=f"{C.URL_JENKINS}{lst_itm[1]}",
+                    target="_blank"
+                )
+            ])
+        else:
+            list_group_item = dbc.ListGroupItem([
+                dbc.Badge(lst_itm[0]),
+                lst_itm[1]
+            ])
+        list_group_items.append(list_group_item)
+
+    if trigger.idx == "tput":
+        title = "Throughput"
+    elif trigger.idx == "bandwidth":
+        title = "Bandwidth"
+    elif trigger.idx == "lat":
+        title = "Latency"
+        hdrh_data = 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(
+                        dcc.Graph(
+                            id="hdrh-latency-graph",
+                            figure=graph_hdrh_latency(hdrh_data, graph_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(
+                    dbc.ListGroup(list_group_items, flush=True),
+                    id="tput-lat-metadata",
+                    class_name="p-0",
+                )
+            ]
+        )
+    ]
+
+    return metadata, graph, True
+
+
+def show_iterative_graph_data(
+        trigger: Trigger,
+        data: dict,
+        graph_layout: dict
+    ) -> tuple:
+    """Generates the data for the offcanvas displayed when a particular point in
+    a box graph (iterative data) is clicked on.
+
+    :param trigger: The information from trigger when the data point is clicked
+        on.
+    :param graph: The data from the clicked point in the graph.
+    :param graph_layout: The layout of the HDRH latency graph.
+    :type trigger: Trigger
+    :type graph: dict
+    :type graph_layout: dict
+    :returns: The data to be displayed on the offcanvas and the information to
+        show the offcanvas.
+    :rtype: tuple(list, list, bool)
+    """
+
+    if trigger.idx == "tput":
+        idx = 0
+    elif trigger.idx == "bandwidth":
+        idx = 1
+    elif trigger.idx == "lat":
+        idx = len(data) - 1
+    else:
+        return list(), list(), False
+
+    try:
+        data = data[idx]["points"]
+    except (IndexError, KeyError, ValueError, TypeError):
+        return list(), list(), False
+
+    def _process_stats(data: list, param: str) -> list:
+        """Process statistical data provided by plot.ly box graph.
+
+        :param data: Statistical data provided by plot.ly box graph.
+        :param param: Parameter saying if the data come from "tput" or
+            "lat" graph.
+        :type data: list
+        :type param: str
+        :returns: Listo of tuples where the first value is the
+            statistic's name and the secont one it's value.
+        :rtype: list
+        """
+        if len(data) == 7:
+            stats = ("max", "upper fence", "q3", "median", "q1",
+                    "lower fence", "min")
+        elif len(data) == 9:
+            stats = ("outlier", "max", "upper fence", "q3", "median",
+                    "q1", "lower fence", "min", "outlier")
+        elif len(data) == 1:
+            if param == "lat":
+                stats = ("average latency at 50% PDR", )
+            elif param == "bandwidth":
+                stats = ("bandwidth", )
+            else:
+                stats = ("throughput", )
+        else:
+            return list()
+        unit = " [us]" if param == "lat" else str()
+        return [(f"{stat}{unit}", f"{value['y']:,.0f}")
+                for stat, value in zip(stats, data)]
+
+    customdata = data[0].get("customdata", dict())
+    datapoint = customdata.get("metadata", dict())
+    hdrh_data = customdata.get("hdrh", dict())
+
+    list_group_items = list()
+    for k, v in datapoint.items():
+        if k == "csit-ref":
+            if len(data) > 1:
+                continue
+            list_group_item = dbc.ListGroupItem([
+                dbc.Badge(k),
+                html.A(v, href=f"{C.URL_JENKINS}{v}", target="_blank")
+            ])
+        else:
+            list_group_item = dbc.ListGroupItem([dbc.Badge(k), v])
+        list_group_items.append(list_group_item)
+
+    graph = list()
+    if trigger.idx == "tput":
+        title = "Throughput"
+    elif trigger.idx == "bandwidth":
+        title = "Bandwidth"
+    elif trigger.idx == "lat":
+        title = "Latency"
+        if len(data) == 1:
+            if hdrh_data:
+                graph = [dbc.Card(
+                    class_name="gy-2 p-0",
+                    children=[
+                        dbc.CardHeader(hdrh_data.pop("name")),
+                        dbc.CardBody(dcc.Graph(
+                            id="hdrh-latency-graph",
+                            figure=graph_hdrh_latency(hdrh_data, graph_layout)
+                        ))
+                    ])
+                ]
+
+    for k, v in _process_stats(data, trigger.idx):
+        list_group_items.append(dbc.ListGroupItem([dbc.Badge(k), v]))
+
+    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(
+                    dbc.ListGroup(list_group_items, flush=True),
+                    id="tput-lat-metadata",
+                    class_name="p-0"
+                )
+            ]
+        )
+    ]
+
+    return metadata, graph, True
index 0d1d6fc..a05379b 100644 (file)
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 
 #!/usr/bin/env python3
 
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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:
index f907d4a..16e094b 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 Cisco and/or its affiliates.
+# Copyright (c) 2024 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:
 # 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: