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.
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(infra, None) is None:
117 tbs[rls][dut][d_ver][infra] = list()
118 if area not in tbs[rls][dut][d_ver][infra]:
119 tbs[rls][dut][d_ver][infra].append(area)
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:
172 dcc.Store(id="store-selected-tests"),
173 dcc.Store(id="store-control-panel"),
174 dcc.Location(id="url", refresh=False),
175 self._add_ctrl_col(),
176 self._add_plotting_col()
194 def _add_navbar(self):
195 """Add nav element with navigation panel. It is placed on the top.
197 :returns: Navigation bar.
198 :rtype: dbc.NavbarSimple
200 return dbc.NavbarSimple(
201 id="navbarsimple-main",
214 brand_external_link=True,
219 def _add_ctrl_col(self) -> dbc.Col:
220 """Add column with controls. It is placed on the left side.
222 :returns: Column with the control panel.
227 children=self._add_ctrl_panel(),
228 className="sticky-top"
232 def _add_plotting_col(self) -> dbc.Col:
233 """Add column with plots. It is placed on the right side.
235 :returns: Column with plots.
239 id="col-plotting-area",
245 class_name="g-0 p-0",
256 def _add_ctrl_panel(self) -> list:
257 """Add control panel.
259 :returns: Control panel.
264 class_name="g-0 p-1",
268 dbc.InputGroupText("CSIT Release"),
270 id={"type": "ctrl-dd", "index": "rls"},
271 placeholder="Select a Release...",
274 {"label": k, "value": k} \
275 for k in self._spec_tbs.keys()
277 key=lambda d: d["label"]
286 class_name="g-0 p-1",
290 dbc.InputGroupText("DUT"),
292 id={"type": "ctrl-dd", "index": "dut"},
293 placeholder="Select a Device under Test..."
301 class_name="g-0 p-1",
305 dbc.InputGroupText("DUT Version"),
307 id={"type": "ctrl-dd", "index": "dutver"},
309 "Select a Version of Device under Test..."
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("Area"),
339 id={"type": "ctrl-dd", "index": "area"},
340 placeholder="Select an Area..."
348 class_name="g-0 p-1",
352 dbc.InputGroupText("Latency"),
356 "value": "show_latency",
357 "label": "Show Latency"
359 value=["show_latency"],
364 style={"align-items": "center"},
371 def _get_plotting_area(
377 """Generate the plotting area with all its content.
379 :param selected: Selected parameters of tests.
380 :param url: URL to be displayed in the modal window.
381 :param show_latency: If True, latency is displayed in the tables.
384 :type show_latency: bool
385 :returns: List of rows with elements to be displayed in the plotting
394 children=coverage_tables(self._data, selected, show_latency),
395 class_name="g-0 p-0",
398 children=C.PLACEHOLDER,
411 "text-transform": "none",
412 "padding": "0rem 1rem"
417 dbc.ModalHeader(dbc.ModalTitle("URL")),
426 id="plot-btn-download",
427 children="Download Data",
431 "text-transform": "none",
432 "padding": "0rem 1rem"
435 dcc.Download(id="download-iterative-data")
438 "d-grid gap-0 d-md-flex justify-content-md-end"
444 children=C.PLACEHOLDER,
449 def callbacks(self, app):
450 """Callbacks for the whole application.
452 :param app: The application.
458 Output("store-control-panel", "data"),
459 Output("store-selected-tests", "data"),
460 Output("plotting-area", "children"),
461 Output({"type": "ctrl-dd", "index": "rls"}, "value"),
462 Output({"type": "ctrl-dd", "index": "dut"}, "options"),
463 Output({"type": "ctrl-dd", "index": "dut"}, "disabled"),
464 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
465 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
466 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
467 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
468 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
469 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
470 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
471 Output({"type": "ctrl-dd", "index": "area"}, "options"),
472 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
473 Output({"type": "ctrl-dd", "index": "area"}, "value"),
474 Output("show-latency", "value"),
477 State("store-control-panel", "data"),
478 State("store-selected-tests", "data")
481 Input("url", "href"),
482 Input("show-latency", "value"),
483 Input({"type": "ctrl-dd", "index": ALL}, "value")
486 def _update_application(
493 """Update the application when the event is detected.
496 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
497 plotting_area = no_update
503 parsed_url = url_decode(href)
505 url_params = parsed_url["params"]
509 trigger = Trigger(callback_context.triggered)
511 if trigger.type == "url" and url_params:
513 show_latency = literal_eval(url_params["show_latency"][0])
514 selected = literal_eval(url_params["selection"][0])
515 except (KeyError, IndexError, AttributeError):
519 "rls-val": selected["rls"],
520 "dut-val": selected["dut"],
521 "dut-opt": generate_options(
522 self._spec_tbs[selected["rls"]].keys()
525 "dutver-val": selected["dutver"],
526 "dutver-opt": generate_options(
527 self._spec_tbs[selected["rls"]]\
528 [selected["dut"]].keys()
531 "phy-val": selected["phy"],
532 "phy-opt": generate_options(
533 self._spec_tbs[selected["rls"]][selected["dut"]]\
534 [selected["dutver"]].keys()
537 "area-val": selected["area"],
539 {"label": label(v), "value": v} for v in sorted(
540 self._spec_tbs[selected["rls"]]\
541 [selected["dut"]][selected["dutver"]]\
546 "show-latency": show_latency
549 elif trigger.type == "show-latency":
550 ctrl_panel.set({"show-latency": show_latency})
552 elif trigger.type == "ctrl-dd":
553 if trigger.idx == "rls":
555 options = generate_options(
556 self._spec_tbs[trigger.value].keys()
563 "rls-val": trigger.value,
568 "dutver-opt": list(),
577 elif trigger.idx == "dut":
579 rls = ctrl_panel.get("rls-val")
580 dut = self._spec_tbs[rls][trigger.value]
581 options = generate_options(dut.keys())
587 "dut-val": trigger.value,
589 "dutver-opt": options,
590 "dutver-dis": disabled,
598 elif trigger.idx == "dutver":
600 rls = ctrl_panel.get("rls-val")
601 dut = ctrl_panel.get("dut-val")
602 dutver = self._spec_tbs[rls][dut][trigger.value]
603 options = generate_options(dutver.keys())
609 "dutver-val": trigger.value,
617 elif trigger.idx == "phy":
619 rls = ctrl_panel.get("rls-val")
620 dut = ctrl_panel.get("dut-val")
621 dutver = ctrl_panel.get("dutver-val")
622 phy = self._spec_tbs[rls][dut][dutver][trigger.value]
624 {"label": label(v), "value": v} for v in sorted(phy)
631 "phy-val": trigger.value,
636 elif trigger.idx == "area":
637 ctrl_panel.set({"area-val": trigger.value})
639 "rls": ctrl_panel.get("rls-val"),
640 "dut": ctrl_panel.get("dut-val"),
641 "dutver": ctrl_panel.get("dutver-val"),
642 "phy": ctrl_panel.get("phy-val"),
643 "area": ctrl_panel.get("area-val"),
649 plotting_area = self._get_plotting_area(
654 "selection": selected,
655 "show_latency": show_latency
658 show_latency=bool(show_latency)
661 plotting_area = C.PLACEHOLDER
669 ret_val.extend(ctrl_panel.values)
673 Output("plot-mod-url", "is_open"),
674 [Input("plot-btn-url", "n_clicks")],
675 [State("plot-mod-url", "is_open")],
677 def toggle_plot_mod_url(n, is_open):
678 """Toggle the modal window with url.
685 Output("download-iterative-data", "data"),
686 State("store-selected-tests", "data"),
687 State("show-latency", "value"),
688 Input("plot-btn-download", "n_clicks"),
689 prevent_initial_call=True
691 def _download_coverage_data(selection, show_latency, _):
694 :param selection: List of tests selected by user stored in the
696 :param show_latency: If True, latency is displayed in the tables.
697 :type selection: dict
698 :type show_latency: bool
699 :returns: dict of data frame content (base64 encoded) and meta data
700 used by the Download component.
707 df = select_coverage_data(
711 show_latency=bool(show_latency)
714 return dcc.send_data_frame(df.to_csv, C.COVERAGE_DOWNLOAD_FILE_NAME)