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:
@@ -127,6 +127,17 @@ def init_app():
             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
 
 
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:
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:
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:
@@ -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.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.
@@ -194,9 +195,7 @@ class Layout:
                     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",
@@ -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.
 
@@ -539,7 +501,7 @@ class Layout:
         ) -> 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
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:
@@ -309,65 +309,3 @@ def comparison_table(
     )
 
     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:
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:
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:
@@ -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.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
 
@@ -161,9 +161,7 @@ class Layout:
                     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",
@@ -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.
 
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:
@@ -57,7 +57,7 @@ def select_coverage_data(
         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) &
@@ -78,8 +78,10 @@ def select_coverage_data(
                 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:
@@ -177,16 +179,20 @@ def select_coverage_data(
 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.
+    :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 start_collapsed: bool
     :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",
-        start_collapsed=True,
+        start_collapsed=start_collapsed,
         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:
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:
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:
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:
@@ -24,7 +24,7 @@ from dash import callback_context
 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
@@ -262,9 +262,7 @@ class Layout:
                     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",
@@ -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.
 
@@ -527,8 +487,8 @@ class Layout:
 
         @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.
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:
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:
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:
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:
@@ -73,8 +73,8 @@ def select_iterative_data(data: pd.DataFrame, itm:dict) -> pd.DataFrame:
     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).
 
@@ -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 sel: dict
+    :param sel: list
     :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:
@@ -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, \
-    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
 
@@ -250,9 +251,7 @@ class Layout:
                     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",
@@ -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.
 
@@ -1365,118 +1327,11 @@ class Layout:
             """
 
             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
 
-            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"),
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:
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:
@@ -33,5 +33,6 @@ def home():
         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:
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:
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:
@@ -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, \
-    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
 
@@ -233,9 +233,7 @@ class Layout:
                     dbc.Row(
                         id="row-navbar",
                         class_name="g-0",
-                        children=[
-                            self._add_navbar()
-                        ]
+                        children=[navbar_trending((False, False, True, False))]
                     ),
                     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.
 
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:
index 7250480..7b0dadc 100644 (file)
           {{ 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
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:
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:
@@ -71,7 +71,7 @@ def graph_trending(
         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).
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:
@@ -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, \
-    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
 
@@ -244,9 +245,7 @@ class Layout:
                     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",
@@ -290,43 +289,6 @@ class Layout:
                 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.
 
@@ -1692,91 +1654,11 @@ class Layout:
             """
 
             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
-
-            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"),
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:
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:
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:
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:
@@ -97,6 +97,12 @@ class Constants:
     # 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 = [
         {
@@ -403,3 +409,18 @@ class Constants:
     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:
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:
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:
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:
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:
@@ -22,11 +22,12 @@ import hdrh.histogram
 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 ..utils.trigger import Trigger
 
 
 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
+
+
+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
 
-# 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:
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: