1 # Copyright (c) 2023 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
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.
56 """The layout of the dash app and the callbacks.
62 data_coverage: pd.DataFrame,
66 - save the input parameters,
67 - prepare data for the control panel,
68 - read HTML layout file,
70 :param app: Flask application running the dash application.
71 :param html_layout_file: Path and name of the file specifying the HTML
72 layout of the dash application.
74 :type html_layout_file: str
79 self._html_layout_file = html_layout_file
80 self._data = data_coverage
82 # Get structure of tests:
84 cols = ["job", "test_id", "dut_version", "release", ]
85 for _, row in self._data[cols].drop_duplicates().iterrows():
87 lst_job = row["job"].split("-")
89 d_ver = row["dut_version"]
90 tbed = "-".join(lst_job[-2:])
91 lst_test_id = row["test_id"].split(".")
95 area = "-".join(lst_test_id[3:-2])
96 suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
98 test = lst_test_id[-1]
99 nic = suite.split("-")[0]
100 for drv in C.DRIVERS:
102 driver = drv.replace("-", "_")
103 test = test.replace(f"{drv}-", "")
107 infra = "-".join((tbed, nic, driver))
109 if tbs.get(rls, None) is None:
111 if tbs[rls].get(dut, None) is None:
112 tbs[rls][dut] = dict()
113 if tbs[rls][dut].get(d_ver, None) is None:
114 tbs[rls][dut][d_ver] = dict()
115 if tbs[rls][dut][d_ver].get(infra, None) is None:
116 tbs[rls][dut][d_ver][infra] = list()
117 if area not in tbs[rls][dut][d_ver][infra]:
118 tbs[rls][dut][d_ver][infra].append(area)
123 self._html_layout = str()
126 with open(self._html_layout_file, "r") as file_read:
127 self._html_layout = file_read.read()
128 except IOError as err:
130 f"Not possible to open the file {self._html_layout_file}\n{err}"
134 if self._app is not None and hasattr(self, "callbacks"):
135 self.callbacks(self._app)
138 def html_layout(self):
139 return self._html_layout
141 def add_content(self):
142 """Top level method which generated the web page.
145 - Store for user input data,
147 - Main area with control panel and ploting area.
149 If no HTML layout is provided, an error message is displayed instead.
151 :returns: The HTML div with the whole page.
155 if self.html_layout and self._spec_tbs:
171 dcc.Store(id="store-selected-tests"),
172 dcc.Store(id="store-control-panel"),
173 dcc.Location(id="url", refresh=False),
174 self._add_ctrl_col(),
175 self._add_plotting_col()
193 def _add_navbar(self):
194 """Add nav element with navigation panel. It is placed on the top.
196 :returns: Navigation bar.
197 :rtype: dbc.NavbarSimple
199 return dbc.NavbarSimple(
200 id="navbarsimple-main",
213 brand_external_link=True,
218 def _add_ctrl_col(self) -> dbc.Col:
219 """Add column with controls. It is placed on the left side.
221 :returns: Column with the control panel.
226 children=self._add_ctrl_panel(),
227 className="sticky-top"
231 def _add_plotting_col(self) -> dbc.Col:
232 """Add column with plots. It is placed on the right side.
234 :returns: Column with plots.
238 id="col-plotting-area",
244 class_name="g-0 p-0",
255 def _add_ctrl_panel(self) -> list:
256 """Add control panel.
258 :returns: Control panel.
263 class_name="g-0 p-1",
267 dbc.InputGroupText("CSIT Release"),
269 id={"type": "ctrl-dd", "index": "rls"},
270 placeholder="Select a Release...",
273 {"label": k, "value": k} \
274 for k in self._spec_tbs.keys()
276 key=lambda d: d["label"]
285 class_name="g-0 p-1",
289 dbc.InputGroupText("DUT"),
291 id={"type": "ctrl-dd", "index": "dut"},
292 placeholder="Select a Device under Test..."
300 class_name="g-0 p-1",
304 dbc.InputGroupText("DUT Version"),
306 id={"type": "ctrl-dd", "index": "dutver"},
308 "Select a Version of Device under Test..."
316 class_name="g-0 p-1",
320 dbc.InputGroupText("Infra"),
322 id={"type": "ctrl-dd", "index": "phy"},
324 "Select a Physical Test Bed Topology..."
332 class_name="g-0 p-1",
336 dbc.InputGroupText("Area"),
338 id={"type": "ctrl-dd", "index": "area"},
339 placeholder="Select an Area..."
348 def _get_plotting_area(self, selected: dict, url: str) -> list:
349 """Generate the plotting area with all its content.
351 :param selected: Selected parameters of tests.
352 :param url: URL to be displayed in the modal window.
355 :returns: List of rows with elements to be displayed in the plotting
364 children=coverage_tables(self._data, selected),
365 class_name="g-0 p-0",
368 children=C.PLACEHOLDER,
381 "text-transform": "none",
382 "padding": "0rem 1rem"
387 dbc.ModalHeader(dbc.ModalTitle("URL")),
396 id="plot-btn-download",
397 children="Download Data",
401 "text-transform": "none",
402 "padding": "0rem 1rem"
405 dcc.Download(id="download-iterative-data")
408 "d-grid gap-0 d-md-flex justify-content-md-end"
414 children=C.PLACEHOLDER,
419 def callbacks(self, app):
420 """Callbacks for the whole application.
422 :param app: The application.
428 Output("store-control-panel", "data"),
429 Output("store-selected-tests", "data"),
430 Output("plotting-area", "children"),
431 Output({"type": "ctrl-dd", "index": "rls"}, "value"),
432 Output({"type": "ctrl-dd", "index": "dut"}, "options"),
433 Output({"type": "ctrl-dd", "index": "dut"}, "disabled"),
434 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
435 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
436 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
437 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
438 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
439 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
440 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
441 Output({"type": "ctrl-dd", "index": "area"}, "options"),
442 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
443 Output({"type": "ctrl-dd", "index": "area"}, "value"),
446 State("store-control-panel", "data"),
447 State("store-selected-tests", "data")
450 Input("url", "href"),
451 Input({"type": "ctrl-dd", "index": ALL}, "value")
454 def _update_application(
460 """Update the application when the event is detected.
463 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
464 plotting_area = no_update
470 parsed_url = url_decode(href)
472 url_params = parsed_url["params"]
476 trigger = Trigger(callback_context.triggered)
478 if trigger.type == "url" and url_params:
480 selected = literal_eval(url_params["selection"][0])
481 except (KeyError, IndexError):
485 "rls-val": selected["rls"],
486 "dut-val": selected["dut"],
487 "dut-opt": generate_options(
488 self._spec_tbs[selected["rls"]].keys()
491 "dutver-val": selected["dutver"],
492 "dutver-opt": generate_options(
493 self._spec_tbs[selected["rls"]]\
494 [selected["dut"]].keys()
497 "phy-val": selected["phy"],
498 "phy-opt": generate_options(
499 self._spec_tbs[selected["rls"]][selected["dut"]]\
500 [selected["dutver"]].keys()
503 "area-val": selected["area"],
505 {"label": label(v), "value": v} for v in sorted(
506 self._spec_tbs[selected["rls"]]\
507 [selected["dut"]][selected["dutver"]]\
514 elif trigger.type == "ctrl-dd":
515 if trigger.idx == "rls":
517 options = generate_options(
518 self._spec_tbs[trigger.value].keys()
525 "rls-val": trigger.value,
530 "dutver-opt": list(),
539 elif trigger.idx == "dut":
541 rls = ctrl_panel.get("rls-val")
542 dut = self._spec_tbs[rls][trigger.value]
543 options = generate_options(dut.keys())
549 "dut-val": trigger.value,
551 "dutver-opt": options,
552 "dutver-dis": disabled,
560 elif trigger.idx == "dutver":
562 rls = ctrl_panel.get("rls-val")
563 dut = ctrl_panel.get("dut-val")
564 dutver = self._spec_tbs[rls][dut][trigger.value]
565 options = generate_options(dutver.keys())
571 "dutver-val": trigger.value,
579 elif trigger.idx == "phy":
581 rls = ctrl_panel.get("rls-val")
582 dut = ctrl_panel.get("dut-val")
583 dutver = ctrl_panel.get("dutver-val")
584 phy = self._spec_tbs[rls][dut][dutver][trigger.value]
586 {"label": label(v), "value": v} for v in sorted(phy)
593 "phy-val": trigger.value,
598 elif trigger.idx == "area":
599 ctrl_panel.set({"area-val": trigger.value})
601 "rls": ctrl_panel.get("rls-val"),
602 "dut": ctrl_panel.get("dut-val"),
603 "dutver": ctrl_panel.get("dutver-val"),
604 "phy": ctrl_panel.get("phy-val"),
605 "area": ctrl_panel.get("area-val"),
611 plotting_area = self._get_plotting_area(
613 gen_new_url(parsed_url, {"selection": selected})
616 plotting_area = C.PLACEHOLDER
624 ret_val.extend(ctrl_panel.values)
628 Output("plot-mod-url", "is_open"),
629 [Input("plot-btn-url", "n_clicks")],
630 [State("plot-mod-url", "is_open")],
632 def toggle_plot_mod_url(n, is_open):
633 """Toggle the modal window with url.
640 Output("download-iterative-data", "data"),
641 State("store-selected-tests", "data"),
642 Input("plot-btn-download", "n_clicks"),
643 prevent_initial_call=True
645 def _download_coverage_data(selection, _):
648 :param selection: List of tests selected by user stored in the
650 :type selection: dict
651 :returns: dict of data frame content (base64 encoded) and meta data
652 used by the Download component.
659 df = select_coverage_data(self._data, selection, csv=True)
661 return dcc.send_data_frame(df.to_csv, C.COVERAGE_DOWNLOAD_FILE_NAME)