feat(core): Adjust T-Rex for external topologies
[csit.git] / csit.infra.dash / app / cdash / coverage / layout.py
index 03d2da7..b8fa023 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:
@@ -15,6 +15,7 @@
 """
 
 
+import logging
 import pandas as pd
 import dash_bootstrap_components as dbc
 
@@ -25,11 +26,13 @@ from dash import callback_context, no_update, ALL
 from dash import Input, Output, State
 from dash.exceptions import PreventUpdate
 from ast import literal_eval
+from yaml import load, FullLoader, YAMLError
 
 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, \
+    show_tooltip
 from ..utils.url_processing import url_decode
 from .tables import coverage_tables, select_coverage_data
 
@@ -48,7 +51,8 @@ CP_PARAMS = {
     "phy-val": str(),
     "area-opt": list(),
     "area-dis": True,
-    "area-val": str()
+    "area-val": str(),
+    "show-latency": ["show_latency", ]
 }
 
 
@@ -60,7 +64,8 @@ class Layout:
             self,
             app: Flask,
             data_coverage: pd.DataFrame,
-            html_layout_file: str
+            html_layout_file: str,
+            tooltip_file: str
         ) -> None:
         """Initialization:
         - save the input parameters,
@@ -70,14 +75,18 @@ class Layout:
         :param app: Flask application running the dash application.
         :param html_layout_file: Path and name of the file specifying the HTML
             layout of the dash application.
+        :param tooltip_file: Path and name of the yaml file specifying the
+            tooltips.
         :type app: Flask
         :type html_layout_file: str
+        :type tooltip_file: str
         """
 
         # Inputs
         self._app = app
-        self._html_layout_file = html_layout_file
         self._data = data_coverage
+        self._html_layout_file = html_layout_file
+        self._tooltip_file = tooltip_file
 
         # Get structure of tests:
         tbs = dict()
@@ -92,7 +101,7 @@ class Layout:
             if dut == "dpdk":
                 area = "dpdk"
             else:
-                area = "-".join(lst_test_id[3:-2])
+                area = ".".join(lst_test_id[3:-2])
             suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
                 replace("2n-", "")
             test = lst_test_id[-1]
@@ -112,10 +121,10 @@ class Layout:
                 tbs[rls][dut] = dict()
             if tbs[rls][dut].get(d_ver, None) is None:
                 tbs[rls][dut][d_ver] = dict()
-            if tbs[rls][dut][d_ver].get(infra, None) is None:
-                tbs[rls][dut][d_ver][infra] = list()
-            if area not in tbs[rls][dut][d_ver][infra]:
-                tbs[rls][dut][d_ver][infra].append(area)
+            if tbs[rls][dut][d_ver].get(area, None) is None:
+                tbs[rls][dut][d_ver][area] = list()
+            if infra not in tbs[rls][dut][d_ver][area]:
+                tbs[rls][dut][d_ver][area].append(infra)
 
         self._spec_tbs = tbs
 
@@ -130,6 +139,19 @@ class Layout:
                 f"Not possible to open the file {self._html_layout_file}\n{err}"
             )
 
+        try:
+            with open(self._tooltip_file, "r") as file_read:
+                self._tooltips = load(file_read, Loader=FullLoader)
+        except IOError as err:
+            logging.warning(
+                f"Not possible to open the file {self._tooltip_file}\n{err}"
+            )
+        except YAMLError as err:
+            logging.warning(
+                f"An error occurred while parsing the specification file "
+                f"{self._tooltip_file}\n{err}"
+            )
+
         # Callbacks:
         if self._app is not None and hasattr(self, "callbacks"):
             self.callbacks(self._app)
@@ -160,9 +182,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",
@@ -174,6 +194,18 @@ class Layout:
                             self._add_ctrl_col(),
                             self._add_plotting_col()
                         ]
+                    ),
+                    dbc.Offcanvas(
+                        class_name="w-75",
+                        id="offcanvas-documentation",
+                        title="Documentation",
+                        placement="end",
+                        is_open=False,
+                        children=html.Iframe(
+                            src=C.URL_DOC_REL_NOTES,
+                            width="100%",
+                            height="100%"
+                        )
                     )
                 ]
             )
@@ -190,31 +222,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.COVERAGE_TITLE,
-                        disabled=True,
-                        external_link=True,
-                        href="#"
-                    )
-                )
-            ],
-            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.
 
@@ -264,7 +271,11 @@ class Layout:
                 children=[
                     dbc.InputGroup(
                         [
-                            dbc.InputGroupText("CSIT Release"),
+                            dbc.InputGroupText(show_tooltip(
+                                self._tooltips,
+                                "help-release",
+                                "CSIT Release"
+                            )),
                             dbc.Select(
                                 id={"type": "ctrl-dd", "index": "rls"},
                                 placeholder="Select a Release...",
@@ -286,7 +297,11 @@ class Layout:
                 children=[
                     dbc.InputGroup(
                         [
-                            dbc.InputGroupText("DUT"),
+                            dbc.InputGroupText(show_tooltip(
+                                self._tooltips,
+                                "help-dut",
+                                "DUT"
+                            )),
                             dbc.Select(
                                 id={"type": "ctrl-dd", "index": "dut"},
                                 placeholder="Select a Device under Test..."
@@ -301,7 +316,11 @@ class Layout:
                 children=[
                     dbc.InputGroup(
                         [
-                            dbc.InputGroupText("DUT Version"),
+                            dbc.InputGroupText(show_tooltip(
+                                self._tooltips,
+                                "help-dut-ver",
+                                "DUT Version"
+                            )),
                             dbc.Select(
                                 id={"type": "ctrl-dd", "index": "dutver"},
                                 placeholder=\
@@ -317,7 +336,30 @@ class Layout:
                 children=[
                     dbc.InputGroup(
                         [
-                            dbc.InputGroupText("Infra"),
+                            dbc.InputGroupText(show_tooltip(
+                                self._tooltips,
+                                "help-area",
+                                "Area"
+                            )),
+                            dbc.Select(
+                                id={"type": "ctrl-dd", "index": "area"},
+                                placeholder="Select an Area..."
+                            )
+                        ],
+                        size="sm"
+                    )
+                ]
+            ),
+            dbc.Row(
+                class_name="g-0 p-1",
+                children=[
+                    dbc.InputGroup(
+                        [
+                            dbc.InputGroupText(show_tooltip(
+                                self._tooltips,
+                                "help-infra",
+                                "Infra"
+                            )),
                             dbc.Select(
                                 id={"type": "ctrl-dd", "index": "phy"},
                                 placeholder=\
@@ -333,25 +375,43 @@ class Layout:
                 children=[
                     dbc.InputGroup(
                         [
-                            dbc.InputGroupText("Area"),
-                            dbc.Select(
-                                id={"type": "ctrl-dd", "index": "area"},
-                                placeholder="Select an Area..."
+                            dbc.InputGroupText(show_tooltip(
+                                self._tooltips,
+                                "help-show-latency",
+                                "Latency"
+                            )),
+                            dbc.Checklist(
+                                id="show-latency",
+                                options=[{
+                                    "value": "show_latency",
+                                    "label": "Show Latency"
+                                }],
+                                value=["show_latency"],
+                                inline=True,
+                                class_name="ms-2"
                             )
                         ],
+                        style={"align-items": "center"},
                         size="sm"
                     )
                 ]
             )
         ]
 
-    def _get_plotting_area(self, selected: dict, url: str) -> list:
+    def _get_plotting_area(
+            self,
+            selected: dict,
+            url: str,
+            show_latency: bool
+        ) -> list:
         """Generate the plotting area with all its content.
 
         :param selected: Selected parameters of tests.
         :param url: URL to be displayed in the modal window.
+        :param show_latency: If True, latency is displayed in the tables.
         :type selected: dict
         :type url: str
+        :type show_latency: bool
         :returns: List of rows with elements to be displayed in the plotting
             area.
         :rtype: list
@@ -361,7 +421,7 @@ class Layout:
 
         return [
             dbc.Row(
-                children=coverage_tables(self._data, selected),
+                children=coverage_tables(self._data, selected, show_latency),
                 class_name="g-0 p-0",
             ),
             dbc.Row(
@@ -441,6 +501,7 @@ class Layout:
                 Output({"type": "ctrl-dd", "index": "area"}, "options"),
                 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
                 Output({"type": "ctrl-dd", "index": "area"}, "value"),
+                Output("show-latency", "value"),
             ],
             [
                 State("store-control-panel", "data"),
@@ -448,6 +509,7 @@ class Layout:
             ],
             [
                 Input("url", "href"),
+                Input("show-latency", "value"),
                 Input({"type": "ctrl-dd", "index": ALL}, "value")
             ]
         )
@@ -455,6 +517,7 @@ class Layout:
                 control_panel: dict,
                 selected: dict,
                 href: str,
+                show_latency: list,
                 *_
             ) -> tuple:
             """Update the application when the event is detected.
@@ -477,8 +540,9 @@ class Layout:
 
             if trigger.type == "url" and url_params:
                 try:
+                    show_latency = literal_eval(url_params["show_latency"][0])
                     selected = literal_eval(url_params["selection"][0])
-                except (KeyError, IndexError):
+                except (KeyError, IndexError, AttributeError):
                     pass
                 if selected:
                     ctrl_panel.set({
@@ -494,23 +558,26 @@ class Layout:
                                 [selected["dut"]].keys()
                         ),
                         "dutver-dis": False,
+                        "area-val": selected["area"],
+                        "area-opt": [
+                            {"label": label(v), "value": v} \
+                                for v in sorted(self._spec_tbs[selected["rls"]]\
+                                    [selected["dut"]]\
+                                        [selected["dutver"]].keys())
+                        ],
+                        "area-dis": False,
                         "phy-val": selected["phy"],
                         "phy-opt": generate_options(
                             self._spec_tbs[selected["rls"]][selected["dut"]]\
-                                [selected["dutver"]].keys()
+                                [selected["dutver"]][selected["area"]]
                         ),
                         "phy-dis": False,
-                        "area-val": selected["area"],
-                        "area-opt": [
-                            {"label": label(v), "value": v} for v in sorted(
-                                self._spec_tbs[selected["rls"]]\
-                                    [selected["dut"]][selected["dutver"]]\
-                                        [selected["phy"]]
-                            )
-                        ],
-                        "area-dis": False
+                        "show-latency": show_latency
                     })
                     on_draw = True
+            elif trigger.type == "show-latency":
+                ctrl_panel.set({"show-latency": show_latency})
+                on_draw = True
             elif trigger.type == "ctrl-dd":
                 if trigger.idx == "rls":
                     try:
@@ -561,42 +628,42 @@ class Layout:
                     try:
                         rls = ctrl_panel.get("rls-val")
                         dut = ctrl_panel.get("dut-val")
-                        dutver = self._spec_tbs[rls][dut][trigger.value]
-                        options = generate_options(dutver.keys())
+                        ver = self._spec_tbs[rls][dut][trigger.value]
+                        options = [
+                            {"label": label(v), "value": v} for v in sorted(ver)
+                        ]
                         disabled = False
                     except KeyError:
                         options = list()
                         disabled = True
                     ctrl_panel.set({
                         "dutver-val": trigger.value,
-                        "phy-val": str(),
-                        "phy-opt": options,
-                        "phy-dis": disabled,
                         "area-val": str(),
-                        "area-opt": list(),
-                        "area-dis": True
+                        "area-opt": options,
+                        "area-dis": disabled,
+                        "phy-val": str(),
+                        "phy-opt": list(),
+                        "phy-dis": True
                     })
-                elif trigger.idx == "phy":
+                elif trigger.idx == "area":
                     try:
                         rls = ctrl_panel.get("rls-val")
                         dut = ctrl_panel.get("dut-val")
-                        dutver = ctrl_panel.get("dutver-val")
-                        phy = self._spec_tbs[rls][dut][dutver][trigger.value]
-                        options = [
-                            {"label": label(v), "value": v} for v in sorted(phy)
-                        ]
+                        ver = ctrl_panel.get("dutver-val")
+                        options = generate_options(
+                            self._spec_tbs[rls][dut][ver][trigger.value])
                         disabled = False
                     except KeyError:
                         options = list()
                         disabled = True
                     ctrl_panel.set({
-                        "phy-val": trigger.value,
-                        "area-val": str(),
-                        "area-opt": options,
-                        "area-dis": disabled
+                        "area-val": trigger.value,
+                        "phy-val": str(),
+                        "phy-opt": options,
+                        "phy-dis": disabled
                     })
-                elif trigger.idx == "area":
-                    ctrl_panel.set({"area-val": trigger.value})
+                elif trigger.idx == "phy":
+                    ctrl_panel.set({"phy-val": trigger.value})
                     selected = {
                         "rls": ctrl_panel.get("rls-val"),
                         "dut": ctrl_panel.get("dut-val"),
@@ -610,7 +677,14 @@ class Layout:
                 if selected:
                     plotting_area = self._get_plotting_area(
                         selected,
-                        gen_new_url(parsed_url, {"selection": selected})
+                        gen_new_url(
+                            parsed_url,
+                            {
+                                "selection": selected,
+                                "show_latency": show_latency
+                            }
+                        ),
+                        show_latency=bool(show_latency)
                     )
                 else:
                     plotting_area = C.PLACEHOLDER
@@ -639,15 +713,18 @@ class Layout:
         @app.callback(
             Output("download-iterative-data", "data"),
             State("store-selected-tests", "data"),
+            State("show-latency", "value"),
             Input("plot-btn-download", "n_clicks"),
             prevent_initial_call=True
         )
-        def _download_coverage_data(selection, _):
+        def _download_coverage_data(selection, show_latency, _):
             """Download the data
 
             :param selection: List of tests selected by user stored in the
                 browser.
+            :param show_latency: If True, latency is displayed in the tables.
             :type selection: dict
+            :type show_latency: bool
             :returns: dict of data frame content (base64 encoded) and meta data
                 used by the Download component.
             :rtype: dict
@@ -656,6 +733,21 @@ class Layout:
             if not selection:
                 raise PreventUpdate
 
-            df = select_coverage_data(self._data, selection, csv=True)
+            df = select_coverage_data(
+                self._data,
+                selection,
+                csv=True,
+                show_latency=bool(show_latency)
+            )
 
             return dcc.send_data_frame(df.to_csv, C.COVERAGE_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