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.
18 import dash_bootstrap_components as dbc
20 from flask import Flask
21 from dash import dcc, html, dash_table, callback_context, no_update, ALL
22 from dash import Input, Output, State
23 from dash.exceptions import PreventUpdate
24 from dash.dash_table.Format import Format, Scheme
25 from ast import literal_eval
27 from ..utils.constants import Constants as C
28 from ..utils.control_panel import ControlPanel
29 from ..utils.trigger import Trigger
30 from ..utils.url_processing import url_decode
31 from ..utils.utils import generate_options, gen_new_url
32 from .tables import comparison_table
35 # Control panel partameters and their default values.
46 "frmsize-opt": list(),
47 "frmsize-val": list(),
50 "cmp-par-opt": list(),
53 "cmp-val-opt": list(),
56 "normalize-val": list()
59 # List of comparable parameters.
61 "dutver": "Release and Version",
62 "infra": "Infrastructure",
63 "frmsize": "Frame Size",
64 "core": "Number of Cores",
65 "ttype": "Measurement"
70 """The layout of the dash app and the callbacks.
76 data_iterative: pd.DataFrame,
80 - save the input parameters,
81 - prepare data for the control panel,
82 - read HTML layout file,
84 :param app: Flask application running the dash application.
85 :param data_iterative: Iterative data to be used in comparison tables.
86 :param html_layout_file: Path and name of the file specifying the HTML
87 layout of the dash application.
89 :type data_iterative: pandas.DataFrame
90 :type html_layout_file: str
95 self._html_layout_file = html_layout_file
96 self._data = data_iterative
98 # Get structure of tests:
101 "job", "test_id", "test_type", "dut_type", "dut_version", "tg_type",
104 for _, row in self._data[cols].drop_duplicates().iterrows():
105 lst_job = row["job"].split("-")
107 dver = f"{row['release']}-{row['dut_version']}"
108 tbed = "-".join(lst_job[-2:])
109 lst_test_id = row["test_id"].split(".")
111 suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
113 test = lst_test_id[-1]
114 nic = suite.split("-")[0]
115 for driver in C.DRIVERS:
117 drv = driver.replace("-", "_")
118 test = test.replace(f"{driver}-", "")
122 infra = "-".join((tbed, nic, drv))
123 lst_test = test.split("-")
125 core = lst_test[1] if lst_test[1] else "8C"
127 if tbs.get(dut, None) is None:
129 if tbs[dut].get(dver, None) is None:
130 tbs[dut][dver] = dict()
131 if tbs[dut][dver].get(infra, None) is None:
132 tbs[dut][dver][infra] = dict()
133 tbs[dut][dver][infra]["core"] = list()
134 tbs[dut][dver][infra]["fsize"] = list()
135 tbs[dut][dver][infra]["ttype"] = list()
136 if core.upper() not in tbs[dut][dver][infra]["core"]:
137 tbs[dut][dver][infra]["core"].append(core.upper())
138 if fsize.upper() not in tbs[dut][dver][infra]["fsize"]:
139 tbs[dut][dver][infra]["fsize"].append(fsize.upper())
140 if row["test_type"] == "mrr":
141 if "MRR" not in tbs[dut][dver][infra]["ttype"]:
142 tbs[dut][dver][infra]["ttype"].append("MRR")
143 elif row["test_type"] == "ndrpdr":
144 if "NDR" not in tbs[dut][dver][infra]["ttype"]:
145 tbs[dut][dver][infra]["ttype"].extend(
146 ("NDR", "PDR", "Latency")
148 elif row["test_type"] == "hoststack" and \
149 row["tg_type"] in ("iperf", "vpp"):
150 if "BPS" not in tbs[dut][dver][infra]["ttype"]:
151 tbs[dut][dver][infra]["ttype"].append("BPS")
152 elif row["test_type"] == "hoststack" and row["tg_type"] == "ab":
153 if "CPS" not in tbs[dut][dver][infra]["ttype"]:
154 tbs[dut][dver][infra]["ttype"].extend(("CPS", "RPS", ))
158 self._html_layout = str()
160 with open(self._html_layout_file, "r") as file_read:
161 self._html_layout = file_read.read()
162 except IOError as err:
164 f"Not possible to open the file {self._html_layout_file}\n{err}"
168 if self._app is not None and hasattr(self, "callbacks"):
169 self.callbacks(self._app)
172 def html_layout(self):
173 return self._html_layout
175 def add_content(self):
176 """Top level method which generated the web page.
179 - Store for user input data,
181 - Main area with control panel and ploting area.
183 If no HTML layout is provided, an error message is displayed instead.
185 :returns: The HTML div with the whole page.
189 if self.html_layout and self._tbs:
205 dcc.Store(id="store-control-panel"),
206 dcc.Store(id="store-selected"),
207 dcc.Location(id="url", refresh=False),
208 self._add_ctrl_col(),
209 self._add_plotting_col()
227 def _add_navbar(self):
228 """Add nav element with navigation panel. It is placed on the top.
230 :returns: Navigation bar.
231 :rtype: dbc.NavbarSimple
233 return dbc.NavbarSimple(
234 id="navbarsimple-main",
247 brand_external_link=True,
252 def _add_ctrl_col(self) -> dbc.Col:
253 """Add column with controls. It is placed on the left side.
255 :returns: Column with the control panel.
260 children=self._add_ctrl_panel(),
261 className="sticky-top"
265 def _add_plotting_col(self) -> dbc.Col:
266 """Add column with plots. It is placed on the right side.
268 :returns: Column with plots.
272 id="col-plotting-area",
278 class_name="g-0 p-0",
289 def _add_ctrl_panel(self) -> list:
290 """Add control panel.
292 :returns: Control panel.
298 class_name="g-0 p-1",
302 dbc.InputGroupText("DUT"),
304 id={"type": "ctrl-dd", "index": "dut"},
305 placeholder="Select a Device under Test...",
308 {"label": k, "value": k} \
309 for k in self._tbs.keys()
311 key=lambda d: d["label"]
320 class_name="g-0 p-1",
324 dbc.InputGroupText("CSIT and DUT Version"),
326 id={"type": "ctrl-dd", "index": "dutver"},
327 placeholder="Select a CSIT and DUT Version...")
334 class_name="g-0 p-1",
338 dbc.InputGroupText("Infra"),
340 id={"type": "ctrl-dd", "index": "infra"},
342 "Select a Physical Test Bed Topology..."
350 class_name="g-0 p-1",
354 dbc.InputGroupText("Frame Size"),
356 id={"type": "ctrl-cl", "index": "frmsize"},
361 style={"align-items": "center"},
367 class_name="g-0 p-1",
371 dbc.InputGroupText("Number of Cores"),
373 id={"type": "ctrl-cl", "index": "core"},
378 style={"align-items": "center"},
384 class_name="g-0 p-1",
388 dbc.InputGroupText("Measurement"),
390 id={"type": "ctrl-cl", "index": "ttype"},
395 style={"align-items": "center"},
404 class_name="g-0 p-1",
408 dbc.InputGroupText("Parameter"),
410 id={"type": "ctrl-dd", "index": "cmpprm"},
411 placeholder="Select a Parameter..."
419 class_name="g-0 p-1",
423 dbc.InputGroupText("Value"),
425 id={"type": "ctrl-dd", "index": "cmpval"},
426 placeholder="Select a Value..."
437 class_name="g-0 p-1",
443 "value": "normalize",
444 "label": "Normalize to 2GHz CPU frequency"
450 style={"align-items": "center"},
462 html.H5("Reference Value")
478 html.H5("Compared Value")
494 html.H5("Normalization")
508 def _get_plotting_area(
514 """Generate the plotting area with all its content.
516 :param selected: Selected parameters of tests.
517 :param normalize: If true, the values in tables are normalized.
518 :param url: URL to be displayed in the modal window.
520 :type normalize: bool
522 :returns: List of rows with elements to be displayed in the plotting
527 title, df = comparison_table(self._data, selected, normalize)
533 "No data for comparison.",
536 class_name="g-0 p-1",
542 for idx, col in enumerate(df.columns):
552 l_col = col.rsplit(" ", 2)
554 "name": [l_col[0], " ".join(l_col[-2:])],
559 "format": Format(precision=2, scheme=Scheme.fixed)
564 children=html.H5(title),
570 children=dash_table.DataTable(
572 data=df.to_dict("records"),
573 merge_duplicate_headers=True,
575 filter_action="native",
576 sort_action="native",
581 style_cell={"textAlign": "right"},
582 style_cell_conditional=[{
583 "if": {"column_id": "Test Name"},
602 "text-transform": "none",
603 "padding": "0rem 1rem"
608 dbc.ModalHeader(dbc.ModalTitle("URL")),
617 id="plot-btn-download",
618 children="Download Data",
622 "text-transform": "none",
623 "padding": "0rem 1rem"
626 dcc.Download(id="download-iterative-data")
629 "d-grid gap-0 d-md-flex justify-content-md-end"
635 children=C.PLACEHOLDER,
640 def callbacks(self, app):
641 """Callbacks for the whole application.
643 :param app: The application.
649 Output("store-control-panel", "data"),
650 Output("store-selected", "data"),
651 Output("plotting-area", "children"),
652 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
653 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
654 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
655 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
656 Output({"type": "ctrl-dd", "index": "infra"}, "options"),
657 Output({"type": "ctrl-dd", "index": "infra"}, "disabled"),
658 Output({"type": "ctrl-dd", "index": "infra"}, "value"),
659 Output({"type": "ctrl-cl", "index": "core"}, "options"),
660 Output({"type": "ctrl-cl", "index": "core"}, "value"),
661 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
662 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
663 Output({"type": "ctrl-cl", "index": "ttype"}, "options"),
664 Output({"type": "ctrl-cl", "index": "ttype"}, "value"),
665 Output({"type": "ctrl-dd", "index": "cmpprm"}, "options"),
666 Output({"type": "ctrl-dd", "index": "cmpprm"}, "disabled"),
667 Output({"type": "ctrl-dd", "index": "cmpprm"}, "value"),
668 Output({"type": "ctrl-dd", "index": "cmpval"}, "options"),
669 Output({"type": "ctrl-dd", "index": "cmpval"}, "disabled"),
670 Output({"type": "ctrl-dd", "index": "cmpval"}, "value"),
671 Output("normalize", "value")
674 State("store-control-panel", "data"),
675 State("store-selected", "data")
678 Input("url", "href"),
679 Input("normalize", "value"),
680 Input({"type": "ctrl-dd", "index": ALL}, "value"),
681 Input({"type": "ctrl-cl", "index": ALL}, "value"),
682 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
685 def _update_application(
692 """Update the application when the event is detected.
695 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
708 parsed_url = url_decode(href)
710 url_params = parsed_url["params"]
715 plotting_area = no_update
717 trigger = Trigger(callback_context.triggered)
718 if trigger.type == "url" and url_params:
721 selected = literal_eval(url_params["selected"][0])
722 r_sel = selected["reference"]["selection"]
723 c_sel = selected["compare"]
724 normalize = literal_eval(url_params["norm"][0])
726 (selected["reference"]["set"] == True) and
727 (c_sel["set"] == True)
729 except (KeyError, IndexError):
733 "dut-val": r_sel["dut"],
734 "dutver-opt": generate_options(
735 self._tbs[r_sel["dut"]].keys()
738 "dutver-val": r_sel["dutver"],
739 "infra-opt": generate_options(
740 self._tbs[r_sel["dut"]][r_sel["dutver"]].keys()
743 "infra-val": r_sel["infra"],
744 "core-opt": generate_options(
745 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
746 [r_sel["infra"]]["core"]
748 "core-val": r_sel["core"],
749 "frmsize-opt": generate_options(
750 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
751 [r_sel["infra"]]["fsize"]
753 "frmsize-val": r_sel["frmsize"],
754 "ttype-opt": generate_options(
755 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
756 [r_sel["infra"]]["ttype"]
758 "ttype-val": r_sel["ttype"],
759 "normalize-val": normalize
762 for itm, label in CMP_PARAMS.items():
763 if len(ctrl_panel.get(f"{itm}-opt")) > 1:
764 opts.append({"label": label, "value": itm})
767 "cmp-par-dis": False,
768 "cmp-par-val": c_sel["parameter"]
771 for itm in ctrl_panel.get(f"{c_sel['parameter']}-opt"):
772 set_val = ctrl_panel.get(f"{c_sel['parameter']}-val")
773 if isinstance(set_val, list):
774 if itm["value"] not in set_val:
777 if itm["value"] != set_val:
781 "cmp-val-dis": False,
782 "cmp-val-val": c_sel["value"]
785 elif trigger.type == "normalize":
786 ctrl_panel.set({"normalize-val": normalize})
788 elif trigger.type == "ctrl-dd":
789 if trigger.idx == "dut":
791 opts = generate_options(self._tbs[trigger.value].keys())
797 "dut-val": trigger.value,
799 "dutver-dis": disabled,
806 "frmsize-opt": list(),
807 "frmsize-val": list(),
810 "cmp-par-opt": list(),
812 "cmp-par-val": str(),
813 "cmp-val-opt": list(),
817 elif trigger.idx == "dutver":
819 dut = ctrl_panel.get("dut-val")
820 dver = self._tbs[dut][trigger.value]
821 opts = generate_options(dver.keys())
827 "dutver-val": trigger.value,
829 "infra-dis": disabled,
833 "frmsize-opt": list(),
834 "frmsize-val": list(),
837 "cmp-par-opt": list(),
839 "cmp-par-val": str(),
840 "cmp-val-opt": list(),
844 elif trigger.idx == "infra":
845 dut = ctrl_panel.get("dut-val")
846 dver = ctrl_panel.get("dutver-val")
847 if all((dut, dver, trigger.value, )):
848 driver = self._tbs[dut][dver][trigger.value]
850 "infra-val": trigger.value,
851 "core-opt": generate_options(driver["core"]),
853 "frmsize-opt": generate_options(driver["fsize"]),
854 "frmsize-val": list(),
855 "ttype-opt": generate_options(driver["ttype"]),
857 "cmp-par-opt": list(),
859 "cmp-par-val": str(),
860 "cmp-val-opt": list(),
864 elif trigger.idx == "cmpprm":
865 value = trigger.value
867 for itm in ctrl_panel.get(f"{value}-opt"):
868 set_val = ctrl_panel.get(f"{value}-val")
869 if isinstance(set_val, list):
870 if itm["value"] == "Latency":
872 if itm["value"] not in set_val:
875 if itm["value"] != set_val:
878 "cmp-par-val": value,
880 "cmp-val-dis": False,
883 elif trigger.idx == "cmpval":
884 ctrl_panel.set({"cmp-val-val": trigger.value})
885 selected["reference"] = {
888 "dut": ctrl_panel.get("dut-val"),
889 "dutver": ctrl_panel.get("dutver-val"),
890 "infra": ctrl_panel.get("infra-val"),
891 "core": ctrl_panel.get("core-val"),
892 "frmsize": ctrl_panel.get("frmsize-val"),
893 "ttype": ctrl_panel.get("ttype-val")
896 selected["compare"] = {
898 "parameter": ctrl_panel.get("cmp-par-val"),
899 "value": trigger.value
902 elif trigger.type == "ctrl-cl":
903 ctrl_panel.set({f"{trigger.idx}-val": trigger.value})
904 if all((ctrl_panel.get("core-val"),
905 ctrl_panel.get("frmsize-val"),
906 ctrl_panel.get("ttype-val"), )):
907 if "Latency" in ctrl_panel.get("ttype-val"):
908 ctrl_panel.set({"ttype-val": ["Latency", ]})
910 for itm, label in CMP_PARAMS.items():
911 if "Latency" in ctrl_panel.get("ttype-val") and \
914 if len(ctrl_panel.get(f"{itm}-opt")) > 1:
915 if isinstance(ctrl_panel.get(f"{itm}-val"), list):
916 if len(ctrl_panel.get(f"{itm}-opt")) == \
917 len(ctrl_panel.get(f"{itm}-val")):
919 opts.append({"label": label, "value": itm})
922 "cmp-par-dis": False,
923 "cmp-par-val": str(),
924 "cmp-val-opt": list(),
930 "cmp-par-opt": list(),
932 "cmp-par-val": str(),
933 "cmp-val-opt": list(),
938 if all((on_draw, selected["reference"]["set"],
939 selected["compare"]["set"], )):
940 plotting_area = self._get_plotting_area(
942 normalize=bool(normalize),
945 params={"selected": selected, "norm": normalize}
949 ret_val = [ctrl_panel.panel, selected, plotting_area]
950 ret_val.extend(ctrl_panel.values)
954 Output("plot-mod-url", "is_open"),
955 Input("plot-btn-url", "n_clicks"),
956 State("plot-mod-url", "is_open")
958 def toggle_plot_mod_url(n, is_open):
959 """Toggle the modal window with url.
966 Output("download-iterative-data", "data"),
967 State("store-selected", "data"),
968 State("normalize", "value"),
969 Input("plot-btn-download", "n_clicks"),
970 prevent_initial_call=True
972 def _download_trending_data(selected: dict, normalize: list, _: int):
973 """Download the data.
975 :param selected: List of tests selected by user stored in the
977 :param normalize: If set, the data is normalized to 2GHz CPU
980 :type normalize: list
981 :returns: dict of data frame content (base64 encoded) and meta data
982 used by the Download component.
989 _, table = comparison_table(self._data, selected, normalize, "csv")
991 return dcc.send_data_frame(table.to_csv, C.COMP_DOWNLOAD_FILE_NAME)