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(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:
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()
181 id="offcanvas-documentation",
182 title="Documentation",
185 children=html.Iframe(
186 src=C.URL_DOC_REL_NOTES,
206 def _add_navbar(self):
207 """Add nav element with navigation panel. It is placed on the top.
209 :returns: Navigation bar.
210 :rtype: dbc.NavbarSimple
212 return dbc.NavbarSimple(
213 id="navbarsimple-main",
215 dbc.NavItem(dbc.NavLink(
220 dbc.NavItem(dbc.NavLink(
225 dbc.NavItem(dbc.NavLink(
231 dbc.NavItem(dbc.NavLink(
233 id="btn-documentation",
238 brand_external_link=True,
243 def _add_ctrl_col(self) -> dbc.Col:
244 """Add column with controls. It is placed on the left side.
246 :returns: Column with the control panel.
251 children=self._add_ctrl_panel(),
252 className="sticky-top"
256 def _add_plotting_col(self) -> dbc.Col:
257 """Add column with plots. It is placed on the right side.
259 :returns: Column with plots.
263 id="col-plotting-area",
269 class_name="g-0 p-0",
280 def _add_ctrl_panel(self) -> list:
281 """Add control panel.
283 :returns: Control panel.
288 class_name="g-0 p-1",
292 dbc.InputGroupText("CSIT Release"),
294 id={"type": "ctrl-dd", "index": "rls"},
295 placeholder="Select a Release...",
298 {"label": k, "value": k} \
299 for k in self._spec_tbs.keys()
301 key=lambda d: d["label"]
310 class_name="g-0 p-1",
314 dbc.InputGroupText("DUT"),
316 id={"type": "ctrl-dd", "index": "dut"},
317 placeholder="Select a Device under Test..."
325 class_name="g-0 p-1",
329 dbc.InputGroupText("DUT Version"),
331 id={"type": "ctrl-dd", "index": "dutver"},
333 "Select a Version of Device under Test..."
341 class_name="g-0 p-1",
345 dbc.InputGroupText("Area"),
347 id={"type": "ctrl-dd", "index": "area"},
348 placeholder="Select an Area..."
356 class_name="g-0 p-1",
360 dbc.InputGroupText("Infra"),
362 id={"type": "ctrl-dd", "index": "phy"},
364 "Select a Physical Test Bed Topology..."
372 class_name="g-0 p-1",
376 dbc.InputGroupText("Latency"),
380 "value": "show_latency",
381 "label": "Show Latency"
383 value=["show_latency"],
388 style={"align-items": "center"},
395 def _get_plotting_area(
401 """Generate the plotting area with all its content.
403 :param selected: Selected parameters of tests.
404 :param url: URL to be displayed in the modal window.
405 :param show_latency: If True, latency is displayed in the tables.
408 :type show_latency: bool
409 :returns: List of rows with elements to be displayed in the plotting
418 children=coverage_tables(self._data, selected, show_latency),
419 class_name="g-0 p-0",
422 children=C.PLACEHOLDER,
435 "text-transform": "none",
436 "padding": "0rem 1rem"
441 dbc.ModalHeader(dbc.ModalTitle("URL")),
450 id="plot-btn-download",
451 children="Download Data",
455 "text-transform": "none",
456 "padding": "0rem 1rem"
459 dcc.Download(id="download-iterative-data")
462 "d-grid gap-0 d-md-flex justify-content-md-end"
468 children=C.PLACEHOLDER,
473 def callbacks(self, app):
474 """Callbacks for the whole application.
476 :param app: The application.
482 Output("store-control-panel", "data"),
483 Output("store-selected-tests", "data"),
484 Output("plotting-area", "children"),
485 Output({"type": "ctrl-dd", "index": "rls"}, "value"),
486 Output({"type": "ctrl-dd", "index": "dut"}, "options"),
487 Output({"type": "ctrl-dd", "index": "dut"}, "disabled"),
488 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
489 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
490 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
491 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
492 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
493 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
494 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
495 Output({"type": "ctrl-dd", "index": "area"}, "options"),
496 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
497 Output({"type": "ctrl-dd", "index": "area"}, "value"),
498 Output("show-latency", "value"),
501 State("store-control-panel", "data"),
502 State("store-selected-tests", "data")
505 Input("url", "href"),
506 Input("show-latency", "value"),
507 Input({"type": "ctrl-dd", "index": ALL}, "value")
510 def _update_application(
517 """Update the application when the event is detected.
520 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
521 plotting_area = no_update
527 parsed_url = url_decode(href)
529 url_params = parsed_url["params"]
533 trigger = Trigger(callback_context.triggered)
535 if trigger.type == "url" and url_params:
537 show_latency = literal_eval(url_params["show_latency"][0])
538 selected = literal_eval(url_params["selection"][0])
539 except (KeyError, IndexError, AttributeError):
543 "rls-val": selected["rls"],
544 "dut-val": selected["dut"],
545 "dut-opt": generate_options(
546 self._spec_tbs[selected["rls"]].keys()
549 "dutver-val": selected["dutver"],
550 "dutver-opt": generate_options(
551 self._spec_tbs[selected["rls"]]\
552 [selected["dut"]].keys()
555 "area-val": selected["area"],
557 {"label": label(v), "value": v} \
558 for v in sorted(self._spec_tbs[selected["rls"]]\
560 [selected["dutver"]].keys())
563 "phy-val": selected["phy"],
564 "phy-opt": generate_options(
565 self._spec_tbs[selected["rls"]][selected["dut"]]\
566 [selected["dutver"]][selected["area"]]
569 "show-latency": show_latency
572 elif trigger.type == "show-latency":
573 ctrl_panel.set({"show-latency": show_latency})
575 elif trigger.type == "ctrl-dd":
576 if trigger.idx == "rls":
578 options = generate_options(
579 self._spec_tbs[trigger.value].keys()
586 "rls-val": trigger.value,
591 "dutver-opt": list(),
600 elif trigger.idx == "dut":
602 rls = ctrl_panel.get("rls-val")
603 dut = self._spec_tbs[rls][trigger.value]
604 options = generate_options(dut.keys())
610 "dut-val": trigger.value,
612 "dutver-opt": options,
613 "dutver-dis": disabled,
621 elif trigger.idx == "dutver":
623 rls = ctrl_panel.get("rls-val")
624 dut = ctrl_panel.get("dut-val")
625 ver = self._spec_tbs[rls][dut][trigger.value]
627 {"label": label(v), "value": v} for v in sorted(ver)
634 "dutver-val": trigger.value,
637 "area-dis": disabled,
642 elif trigger.idx == "area":
644 rls = ctrl_panel.get("rls-val")
645 dut = ctrl_panel.get("dut-val")
646 ver = ctrl_panel.get("dutver-val")
647 options = generate_options(
648 self._spec_tbs[rls][dut][ver][trigger.value])
654 "area-val": trigger.value,
659 elif trigger.idx == "phy":
660 ctrl_panel.set({"phy-val": trigger.value})
662 "rls": ctrl_panel.get("rls-val"),
663 "dut": ctrl_panel.get("dut-val"),
664 "dutver": ctrl_panel.get("dutver-val"),
665 "phy": ctrl_panel.get("phy-val"),
666 "area": ctrl_panel.get("area-val"),
672 plotting_area = self._get_plotting_area(
677 "selection": selected,
678 "show_latency": show_latency
681 show_latency=bool(show_latency)
684 plotting_area = C.PLACEHOLDER
692 ret_val.extend(ctrl_panel.values)
696 Output("plot-mod-url", "is_open"),
697 [Input("plot-btn-url", "n_clicks")],
698 [State("plot-mod-url", "is_open")],
700 def toggle_plot_mod_url(n, is_open):
701 """Toggle the modal window with url.
708 Output("download-iterative-data", "data"),
709 State("store-selected-tests", "data"),
710 State("show-latency", "value"),
711 Input("plot-btn-download", "n_clicks"),
712 prevent_initial_call=True
714 def _download_coverage_data(selection, show_latency, _):
717 :param selection: List of tests selected by user stored in the
719 :param show_latency: If True, latency is displayed in the tables.
720 :type selection: dict
721 :type show_latency: bool
722 :returns: dict of data frame content (base64 encoded) and meta data
723 used by the Download component.
730 df = select_coverage_data(
734 show_latency=bool(show_latency)
737 return dcc.send_data_frame(df.to_csv, C.COVERAGE_DOWNLOAD_FILE_NAME)
740 Output("offcanvas-documentation", "is_open"),
741 Input("btn-documentation", "n_clicks"),
742 State("offcanvas-documentation", "is_open")
744 def toggle_offcanvas_documentation(n_clicks, is_open):