1 # Copyright (c) 2024 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 """Plotly Dash HTML layout override.
20 import dash_bootstrap_components as dbc
22 from flask import Flask
25 from dash import callback_context, no_update, ALL
26 from dash import Input, Output, State
27 from dash.exceptions import PreventUpdate
28 from ast import literal_eval
29 from yaml import load, FullLoader, YAMLError
31 from ..utils.constants import Constants as C
32 from ..utils.control_panel import ControlPanel
33 from ..utils.trigger import Trigger
34 from ..utils.utils import label, gen_new_url, generate_options, navbar_report, \
36 from ..utils.url_processing import url_decode
37 from .tables import coverage_tables, select_coverage_data
40 # Control panel partameters and their default values.
55 "show-latency": ["show_latency", ]
60 """The layout of the dash app and the callbacks.
66 data_coverage: pd.DataFrame,
67 html_layout_file: str,
71 - save the input parameters,
72 - prepare data for the control panel,
73 - read HTML layout file,
75 :param app: Flask application running the dash application.
76 :param html_layout_file: Path and name of the file specifying the HTML
77 layout of the dash application.
78 :param tooltip_file: Path and name of the yaml file specifying the
81 :type html_layout_file: str
82 :type tooltip_file: str
87 self._data = data_coverage
88 self._html_layout_file = html_layout_file
89 self._tooltip_file = tooltip_file
91 # Get structure of tests:
93 cols = ["job", "test_id", "dut_version", "release", ]
94 for _, row in self._data[cols].drop_duplicates().iterrows():
96 lst_job = row["job"].split("-")
98 d_ver = row["dut_version"]
99 tbed = "-".join(lst_job[-2:])
100 lst_test_id = row["test_id"].split(".")
104 area = ".".join(lst_test_id[3:-2])
105 suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
107 test = lst_test_id[-1]
108 nic = suite.split("-")[0]
109 for drv in C.DRIVERS:
111 driver = drv.replace("-", "_")
112 test = test.replace(f"{drv}-", "")
116 infra = "-".join((tbed, nic, driver))
118 if tbs.get(rls, None) is None:
120 if tbs[rls].get(dut, None) is None:
121 tbs[rls][dut] = dict()
122 if tbs[rls][dut].get(d_ver, None) is None:
123 tbs[rls][dut][d_ver] = dict()
124 if tbs[rls][dut][d_ver].get(area, None) is None:
125 tbs[rls][dut][d_ver][area] = list()
126 if infra not in tbs[rls][dut][d_ver][area]:
127 tbs[rls][dut][d_ver][area].append(infra)
132 self._html_layout = str()
135 with open(self._html_layout_file, "r") as file_read:
136 self._html_layout = file_read.read()
137 except IOError as err:
139 f"Not possible to open the file {self._html_layout_file}\n{err}"
143 with open(self._tooltip_file, "r") as file_read:
144 self._tooltips = load(file_read, Loader=FullLoader)
145 except IOError as err:
147 f"Not possible to open the file {self._tooltip_file}\n{err}"
149 except YAMLError as err:
151 f"An error occurred while parsing the specification file "
152 f"{self._tooltip_file}\n{err}"
156 if self._app is not None and hasattr(self, "callbacks"):
157 self.callbacks(self._app)
160 def html_layout(self):
161 return self._html_layout
163 def add_content(self):
164 """Top level method which generated the web page.
167 - Store for user input data,
169 - Main area with control panel and ploting area.
171 If no HTML layout is provided, an error message is displayed instead.
173 :returns: The HTML div with the whole page.
177 if self.html_layout and self._spec_tbs:
185 children=[navbar_report((False, False, True, False)), ]
191 dcc.Store(id="store-selected-tests"),
192 dcc.Store(id="store-control-panel"),
193 dcc.Location(id="url", refresh=False),
194 self._add_ctrl_col(),
195 self._add_plotting_col()
200 id="offcanvas-documentation",
201 title="Documentation",
204 children=html.Iframe(
205 src=C.URL_DOC_REL_NOTES,
225 def _add_ctrl_col(self) -> dbc.Col:
226 """Add column with controls. It is placed on the left side.
228 :returns: Column with the control panel.
233 children=self._add_ctrl_panel(),
234 className="sticky-top"
238 def _add_plotting_col(self) -> dbc.Col:
239 """Add column with plots. It is placed on the right side.
241 :returns: Column with plots.
245 id="col-plotting-area",
251 class_name="g-0 p-0",
262 def _add_ctrl_panel(self) -> list:
263 """Add control panel.
265 :returns: Control panel.
270 class_name="g-0 p-1",
274 dbc.InputGroupText(show_tooltip(
280 id={"type": "ctrl-dd", "index": "rls"},
281 placeholder="Select a Release...",
284 {"label": k, "value": k} \
285 for k in self._spec_tbs.keys()
287 key=lambda d: d["label"]
296 class_name="g-0 p-1",
300 dbc.InputGroupText(show_tooltip(
306 id={"type": "ctrl-dd", "index": "dut"},
307 placeholder="Select a Device under Test..."
315 class_name="g-0 p-1",
319 dbc.InputGroupText(show_tooltip(
325 id={"type": "ctrl-dd", "index": "dutver"},
327 "Select a Version of Device under Test..."
335 class_name="g-0 p-1",
339 dbc.InputGroupText(show_tooltip(
345 id={"type": "ctrl-dd", "index": "area"},
346 placeholder="Select an Area..."
354 class_name="g-0 p-1",
358 dbc.InputGroupText(show_tooltip(
364 id={"type": "ctrl-dd", "index": "phy"},
366 "Select a Physical Test Bed Topology..."
374 class_name="g-0 p-1",
378 dbc.InputGroupText(show_tooltip(
386 "value": "show_latency",
387 "label": "Show Latency"
389 value=["show_latency"],
394 style={"align-items": "center"},
401 def _get_plotting_area(
407 """Generate the plotting area with all its content.
409 :param selected: Selected parameters of tests.
410 :param url: URL to be displayed in the modal window.
411 :param show_latency: If True, latency is displayed in the tables.
414 :type show_latency: bool
415 :returns: List of rows with elements to be displayed in the plotting
424 children=coverage_tables(self._data, selected, show_latency),
425 class_name="g-0 p-0",
428 children=C.PLACEHOLDER,
441 "text-transform": "none",
442 "padding": "0rem 1rem"
447 dbc.ModalHeader(dbc.ModalTitle("URL")),
456 id="plot-btn-download",
457 children="Download Data",
461 "text-transform": "none",
462 "padding": "0rem 1rem"
465 dcc.Download(id="download-iterative-data")
468 "d-grid gap-0 d-md-flex justify-content-md-end"
474 children=C.PLACEHOLDER,
479 def callbacks(self, app):
480 """Callbacks for the whole application.
482 :param app: The application.
488 Output("store-control-panel", "data"),
489 Output("store-selected-tests", "data"),
490 Output("plotting-area", "children"),
491 Output({"type": "ctrl-dd", "index": "rls"}, "value"),
492 Output({"type": "ctrl-dd", "index": "dut"}, "options"),
493 Output({"type": "ctrl-dd", "index": "dut"}, "disabled"),
494 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
495 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
496 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
497 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
498 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
499 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
500 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
501 Output({"type": "ctrl-dd", "index": "area"}, "options"),
502 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
503 Output({"type": "ctrl-dd", "index": "area"}, "value"),
504 Output("show-latency", "value"),
507 State("store-control-panel", "data"),
508 State("store-selected-tests", "data")
511 Input("url", "href"),
512 Input("show-latency", "value"),
513 Input({"type": "ctrl-dd", "index": ALL}, "value")
516 def _update_application(
523 """Update the application when the event is detected.
526 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
527 plotting_area = no_update
533 parsed_url = url_decode(href)
535 url_params = parsed_url["params"]
539 trigger = Trigger(callback_context.triggered)
541 if trigger.type == "url" and url_params:
543 show_latency = literal_eval(url_params["show_latency"][0])
544 selected = literal_eval(url_params["selection"][0])
545 except (KeyError, IndexError, AttributeError):
549 "rls-val": selected["rls"],
550 "dut-val": selected["dut"],
551 "dut-opt": generate_options(
552 self._spec_tbs[selected["rls"]].keys()
555 "dutver-val": selected["dutver"],
556 "dutver-opt": generate_options(
557 self._spec_tbs[selected["rls"]]\
558 [selected["dut"]].keys()
561 "area-val": selected["area"],
563 {"label": label(v), "value": v} \
564 for v in sorted(self._spec_tbs[selected["rls"]]\
566 [selected["dutver"]].keys())
569 "phy-val": selected["phy"],
570 "phy-opt": generate_options(
571 self._spec_tbs[selected["rls"]][selected["dut"]]\
572 [selected["dutver"]][selected["area"]]
575 "show-latency": show_latency
578 elif trigger.type == "show-latency":
579 ctrl_panel.set({"show-latency": show_latency})
581 elif trigger.type == "ctrl-dd":
582 if trigger.idx == "rls":
584 options = generate_options(
585 self._spec_tbs[trigger.value].keys()
592 "rls-val": trigger.value,
597 "dutver-opt": list(),
606 elif trigger.idx == "dut":
608 rls = ctrl_panel.get("rls-val")
609 dut = self._spec_tbs[rls][trigger.value]
610 options = generate_options(dut.keys())
616 "dut-val": trigger.value,
618 "dutver-opt": options,
619 "dutver-dis": disabled,
627 elif trigger.idx == "dutver":
629 rls = ctrl_panel.get("rls-val")
630 dut = ctrl_panel.get("dut-val")
631 ver = self._spec_tbs[rls][dut][trigger.value]
633 {"label": label(v), "value": v} for v in sorted(ver)
640 "dutver-val": trigger.value,
643 "area-dis": disabled,
648 elif trigger.idx == "area":
650 rls = ctrl_panel.get("rls-val")
651 dut = ctrl_panel.get("dut-val")
652 ver = ctrl_panel.get("dutver-val")
653 options = generate_options(
654 self._spec_tbs[rls][dut][ver][trigger.value])
660 "area-val": trigger.value,
665 elif trigger.idx == "phy":
666 ctrl_panel.set({"phy-val": trigger.value})
668 "rls": ctrl_panel.get("rls-val"),
669 "dut": ctrl_panel.get("dut-val"),
670 "dutver": ctrl_panel.get("dutver-val"),
671 "phy": ctrl_panel.get("phy-val"),
672 "area": ctrl_panel.get("area-val"),
678 plotting_area = self._get_plotting_area(
683 "selection": selected,
684 "show_latency": show_latency
687 show_latency=bool(show_latency)
690 plotting_area = C.PLACEHOLDER
698 ret_val.extend(ctrl_panel.values)
702 Output("plot-mod-url", "is_open"),
703 [Input("plot-btn-url", "n_clicks")],
704 [State("plot-mod-url", "is_open")],
706 def toggle_plot_mod_url(n, is_open):
707 """Toggle the modal window with url.
714 Output("download-iterative-data", "data"),
715 State("store-selected-tests", "data"),
716 State("show-latency", "value"),
717 Input("plot-btn-download", "n_clicks"),
718 prevent_initial_call=True
720 def _download_coverage_data(selection, show_latency, _):
723 :param selection: List of tests selected by user stored in the
725 :param show_latency: If True, latency is displayed in the tables.
726 :type selection: dict
727 :type show_latency: bool
728 :returns: dict of data frame content (base64 encoded) and meta data
729 used by the Download component.
736 df = select_coverage_data(
740 show_latency=bool(show_latency)
743 return dcc.send_data_frame(df.to_csv, C.COVERAGE_DOWNLOAD_FILE_NAME)
746 Output("offcanvas-documentation", "is_open"),
747 Input("btn-documentation", "n_clicks"),
748 State("offcanvas-documentation", "is_open")
750 def toggle_offcanvas_documentation(n_clicks, is_open):