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.
19 import dash_bootstrap_components as dbc
21 from flask import Flask
24 from dash import callback_context, no_update, ALL
25 from dash import Input, Output, State
26 from dash.exceptions import PreventUpdate
27 from ast import literal_eval
29 from ..utils.constants import Constants as C
30 from ..utils.control_panel import ControlPanel
31 from ..utils.trigger import Trigger
32 from ..utils.utils import label, gen_new_url, generate_options, navbar_report
33 from ..utils.url_processing import url_decode
34 from .tables import coverage_tables, select_coverage_data
37 # Control panel partameters and their default values.
52 "show-latency": ["show_latency", ]
57 """The layout of the dash app and the callbacks.
63 data_coverage: pd.DataFrame,
67 - save the input parameters,
68 - prepare data for the control panel,
69 - read HTML layout file,
71 :param app: Flask application running the dash application.
72 :param html_layout_file: Path and name of the file specifying the HTML
73 layout of the dash application.
75 :type html_layout_file: str
80 self._html_layout_file = html_layout_file
81 self._data = data_coverage
83 # Get structure of tests:
85 cols = ["job", "test_id", "dut_version", "release", ]
86 for _, row in self._data[cols].drop_duplicates().iterrows():
88 lst_job = row["job"].split("-")
90 d_ver = row["dut_version"]
91 tbed = "-".join(lst_job[-2:])
92 lst_test_id = row["test_id"].split(".")
96 area = ".".join(lst_test_id[3:-2])
97 suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
99 test = lst_test_id[-1]
100 nic = suite.split("-")[0]
101 for drv in C.DRIVERS:
103 driver = drv.replace("-", "_")
104 test = test.replace(f"{drv}-", "")
108 infra = "-".join((tbed, nic, driver))
110 if tbs.get(rls, None) is None:
112 if tbs[rls].get(dut, None) is None:
113 tbs[rls][dut] = dict()
114 if tbs[rls][dut].get(d_ver, None) is None:
115 tbs[rls][dut][d_ver] = dict()
116 if tbs[rls][dut][d_ver].get(area, None) is None:
117 tbs[rls][dut][d_ver][area] = list()
118 if infra not in tbs[rls][dut][d_ver][area]:
119 tbs[rls][dut][d_ver][area].append(infra)
124 self._html_layout = str()
127 with open(self._html_layout_file, "r") as file_read:
128 self._html_layout = file_read.read()
129 except IOError as err:
131 f"Not possible to open the file {self._html_layout_file}\n{err}"
135 if self._app is not None and hasattr(self, "callbacks"):
136 self.callbacks(self._app)
139 def html_layout(self):
140 return self._html_layout
142 def add_content(self):
143 """Top level method which generated the web page.
146 - Store for user input data,
148 - Main area with control panel and ploting area.
150 If no HTML layout is provided, an error message is displayed instead.
152 :returns: The HTML div with the whole page.
156 if self.html_layout and self._spec_tbs:
164 children=[navbar_report((False, False, True, False)), ]
170 dcc.Store(id="store-selected-tests"),
171 dcc.Store(id="store-control-panel"),
172 dcc.Location(id="url", refresh=False),
173 self._add_ctrl_col(),
174 self._add_plotting_col()
179 id="offcanvas-documentation",
180 title="Documentation",
183 children=html.Iframe(
184 src=C.URL_DOC_REL_NOTES,
204 def _add_ctrl_col(self) -> dbc.Col:
205 """Add column with controls. It is placed on the left side.
207 :returns: Column with the control panel.
212 children=self._add_ctrl_panel(),
213 className="sticky-top"
217 def _add_plotting_col(self) -> dbc.Col:
218 """Add column with plots. It is placed on the right side.
220 :returns: Column with plots.
224 id="col-plotting-area",
230 class_name="g-0 p-0",
241 def _add_ctrl_panel(self) -> list:
242 """Add control panel.
244 :returns: Control panel.
249 class_name="g-0 p-1",
253 dbc.InputGroupText("CSIT Release"),
255 id={"type": "ctrl-dd", "index": "rls"},
256 placeholder="Select a Release...",
259 {"label": k, "value": k} \
260 for k in self._spec_tbs.keys()
262 key=lambda d: d["label"]
271 class_name="g-0 p-1",
275 dbc.InputGroupText("DUT"),
277 id={"type": "ctrl-dd", "index": "dut"},
278 placeholder="Select a Device under Test..."
286 class_name="g-0 p-1",
290 dbc.InputGroupText("DUT Version"),
292 id={"type": "ctrl-dd", "index": "dutver"},
294 "Select a Version of Device under Test..."
302 class_name="g-0 p-1",
306 dbc.InputGroupText("Area"),
308 id={"type": "ctrl-dd", "index": "area"},
309 placeholder="Select an Area..."
317 class_name="g-0 p-1",
321 dbc.InputGroupText("Infra"),
323 id={"type": "ctrl-dd", "index": "phy"},
325 "Select a Physical Test Bed Topology..."
333 class_name="g-0 p-1",
337 dbc.InputGroupText("Latency"),
341 "value": "show_latency",
342 "label": "Show Latency"
344 value=["show_latency"],
349 style={"align-items": "center"},
356 def _get_plotting_area(
362 """Generate the plotting area with all its content.
364 :param selected: Selected parameters of tests.
365 :param url: URL to be displayed in the modal window.
366 :param show_latency: If True, latency is displayed in the tables.
369 :type show_latency: bool
370 :returns: List of rows with elements to be displayed in the plotting
379 children=coverage_tables(self._data, selected, show_latency),
380 class_name="g-0 p-0",
383 children=C.PLACEHOLDER,
396 "text-transform": "none",
397 "padding": "0rem 1rem"
402 dbc.ModalHeader(dbc.ModalTitle("URL")),
411 id="plot-btn-download",
412 children="Download Data",
416 "text-transform": "none",
417 "padding": "0rem 1rem"
420 dcc.Download(id="download-iterative-data")
423 "d-grid gap-0 d-md-flex justify-content-md-end"
429 children=C.PLACEHOLDER,
434 def callbacks(self, app):
435 """Callbacks for the whole application.
437 :param app: The application.
443 Output("store-control-panel", "data"),
444 Output("store-selected-tests", "data"),
445 Output("plotting-area", "children"),
446 Output({"type": "ctrl-dd", "index": "rls"}, "value"),
447 Output({"type": "ctrl-dd", "index": "dut"}, "options"),
448 Output({"type": "ctrl-dd", "index": "dut"}, "disabled"),
449 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
450 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
451 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
452 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
453 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
454 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
455 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
456 Output({"type": "ctrl-dd", "index": "area"}, "options"),
457 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
458 Output({"type": "ctrl-dd", "index": "area"}, "value"),
459 Output("show-latency", "value"),
462 State("store-control-panel", "data"),
463 State("store-selected-tests", "data")
466 Input("url", "href"),
467 Input("show-latency", "value"),
468 Input({"type": "ctrl-dd", "index": ALL}, "value")
471 def _update_application(
478 """Update the application when the event is detected.
481 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
482 plotting_area = no_update
488 parsed_url = url_decode(href)
490 url_params = parsed_url["params"]
494 trigger = Trigger(callback_context.triggered)
496 if trigger.type == "url" and url_params:
498 show_latency = literal_eval(url_params["show_latency"][0])
499 selected = literal_eval(url_params["selection"][0])
500 except (KeyError, IndexError, AttributeError):
504 "rls-val": selected["rls"],
505 "dut-val": selected["dut"],
506 "dut-opt": generate_options(
507 self._spec_tbs[selected["rls"]].keys()
510 "dutver-val": selected["dutver"],
511 "dutver-opt": generate_options(
512 self._spec_tbs[selected["rls"]]\
513 [selected["dut"]].keys()
516 "area-val": selected["area"],
518 {"label": label(v), "value": v} \
519 for v in sorted(self._spec_tbs[selected["rls"]]\
521 [selected["dutver"]].keys())
524 "phy-val": selected["phy"],
525 "phy-opt": generate_options(
526 self._spec_tbs[selected["rls"]][selected["dut"]]\
527 [selected["dutver"]][selected["area"]]
530 "show-latency": show_latency
533 elif trigger.type == "show-latency":
534 ctrl_panel.set({"show-latency": show_latency})
536 elif trigger.type == "ctrl-dd":
537 if trigger.idx == "rls":
539 options = generate_options(
540 self._spec_tbs[trigger.value].keys()
547 "rls-val": trigger.value,
552 "dutver-opt": list(),
561 elif trigger.idx == "dut":
563 rls = ctrl_panel.get("rls-val")
564 dut = self._spec_tbs[rls][trigger.value]
565 options = generate_options(dut.keys())
571 "dut-val": trigger.value,
573 "dutver-opt": options,
574 "dutver-dis": disabled,
582 elif trigger.idx == "dutver":
584 rls = ctrl_panel.get("rls-val")
585 dut = ctrl_panel.get("dut-val")
586 ver = self._spec_tbs[rls][dut][trigger.value]
588 {"label": label(v), "value": v} for v in sorted(ver)
595 "dutver-val": trigger.value,
598 "area-dis": disabled,
603 elif trigger.idx == "area":
605 rls = ctrl_panel.get("rls-val")
606 dut = ctrl_panel.get("dut-val")
607 ver = ctrl_panel.get("dutver-val")
608 options = generate_options(
609 self._spec_tbs[rls][dut][ver][trigger.value])
615 "area-val": trigger.value,
620 elif trigger.idx == "phy":
621 ctrl_panel.set({"phy-val": trigger.value})
623 "rls": ctrl_panel.get("rls-val"),
624 "dut": ctrl_panel.get("dut-val"),
625 "dutver": ctrl_panel.get("dutver-val"),
626 "phy": ctrl_panel.get("phy-val"),
627 "area": ctrl_panel.get("area-val"),
633 plotting_area = self._get_plotting_area(
638 "selection": selected,
639 "show_latency": show_latency
642 show_latency=bool(show_latency)
645 plotting_area = C.PLACEHOLDER
653 ret_val.extend(ctrl_panel.values)
657 Output("plot-mod-url", "is_open"),
658 [Input("plot-btn-url", "n_clicks")],
659 [State("plot-mod-url", "is_open")],
661 def toggle_plot_mod_url(n, is_open):
662 """Toggle the modal window with url.
669 Output("download-iterative-data", "data"),
670 State("store-selected-tests", "data"),
671 State("show-latency", "value"),
672 Input("plot-btn-download", "n_clicks"),
673 prevent_initial_call=True
675 def _download_coverage_data(selection, show_latency, _):
678 :param selection: List of tests selected by user stored in the
680 :param show_latency: If True, latency is displayed in the tables.
681 :type selection: dict
682 :type show_latency: bool
683 :returns: dict of data frame content (base64 encoded) and meta data
684 used by the Download component.
691 df = select_coverage_data(
695 show_latency=bool(show_latency)
698 return dcc.send_data_frame(df.to_csv, C.COVERAGE_DOWNLOAD_FILE_NAME)
701 Output("offcanvas-documentation", "is_open"),
702 Input("btn-documentation", "n_clicks"),
703 State("offcanvas-documentation", "is_open")
705 def toggle_offcanvas_documentation(n_clicks, is_open):