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.
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, navbar_report, \
33 from .tables import comparison_table
36 # Control panel partameters and their default values.
47 "frmsize-opt": list(),
48 "frmsize-val": list(),
51 "cmp-par-opt": list(),
54 "cmp-val-opt": list(),
57 "normalize-val": list()
60 # List of comparable parameters.
62 "dutver": "Release and Version",
63 "infra": "Infrastructure",
64 "frmsize": "Frame Size",
65 "core": "Number of Cores",
66 "ttype": "Measurement"
71 """The layout of the dash app and the callbacks.
77 data_iterative: pd.DataFrame,
81 - save the input parameters,
82 - prepare data for the control panel,
83 - read HTML layout file,
85 :param app: Flask application running the dash application.
86 :param data_iterative: Iterative data to be used in comparison tables.
87 :param html_layout_file: Path and name of the file specifying the HTML
88 layout of the dash application.
90 :type data_iterative: pandas.DataFrame
91 :type html_layout_file: str
96 self._html_layout_file = html_layout_file
97 self._data = data_iterative
99 # Get structure of tests:
102 "job", "test_id", "test_type", "dut_type", "dut_version", "tg_type",
105 for _, row in self._data[cols].drop_duplicates().iterrows():
106 lst_job = row["job"].split("-")
108 dver = f"{row['release']}-{row['dut_version']}"
109 tbed = "-".join(lst_job[-2:])
110 lst_test_id = row["test_id"].split(".")
112 suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
114 test = lst_test_id[-1]
115 nic = suite.split("-")[0]
116 for driver in C.DRIVERS:
118 drv = driver.replace("-", "_")
119 test = test.replace(f"{driver}-", "")
123 infra = "-".join((tbed, nic, drv))
124 lst_test = test.split("-")
126 core = lst_test[1] if lst_test[1] else "8C"
128 if tbs.get(dut, None) is None:
130 if tbs[dut].get(dver, None) is None:
131 tbs[dut][dver] = dict()
132 if tbs[dut][dver].get(infra, None) is None:
133 tbs[dut][dver][infra] = dict()
134 tbs[dut][dver][infra]["core"] = list()
135 tbs[dut][dver][infra]["fsize"] = list()
136 tbs[dut][dver][infra]["ttype"] = list()
137 if core.upper() not in tbs[dut][dver][infra]["core"]:
138 tbs[dut][dver][infra]["core"].append(core.upper())
139 if fsize.upper() not in tbs[dut][dver][infra]["fsize"]:
140 tbs[dut][dver][infra]["fsize"].append(fsize.upper())
141 if row["test_type"] == "mrr":
142 if "MRR" not in tbs[dut][dver][infra]["ttype"]:
143 tbs[dut][dver][infra]["ttype"].append("MRR")
144 elif row["test_type"] == "ndrpdr":
145 if "NDR" not in tbs[dut][dver][infra]["ttype"]:
146 tbs[dut][dver][infra]["ttype"].extend(
147 ("NDR", "PDR", "Latency")
149 elif row["test_type"] == "hoststack" and \
150 row["tg_type"] in ("iperf", "vpp"):
151 if "BPS" not in tbs[dut][dver][infra]["ttype"]:
152 tbs[dut][dver][infra]["ttype"].append("BPS")
153 elif row["test_type"] == "hoststack" and row["tg_type"] == "ab":
154 if "CPS" not in tbs[dut][dver][infra]["ttype"]:
155 tbs[dut][dver][infra]["ttype"].extend(("CPS", "RPS", ))
159 self._html_layout = str()
161 with open(self._html_layout_file, "r") as file_read:
162 self._html_layout = file_read.read()
163 except IOError as err:
165 f"Not possible to open the file {self._html_layout_file}\n{err}"
169 if self._app is not None and hasattr(self, "callbacks"):
170 self.callbacks(self._app)
173 def html_layout(self):
174 return self._html_layout
176 def add_content(self):
177 """Top level method which generated the web page.
180 - Store for user input data,
182 - Main area with control panel and ploting area.
184 If no HTML layout is provided, an error message is displayed instead.
186 :returns: The HTML div with the whole page.
190 if self.html_layout and self._tbs:
198 children=[navbar_report((False, True, False, False)), ]
204 dcc.Store(id="store-control-panel"),
205 dcc.Store(id="store-selected"),
206 dcc.Store(id="store-table-data"),
207 dcc.Store(id="store-filtered-table-data"),
208 dcc.Location(id="url", refresh=False),
209 self._add_ctrl_col(),
210 self._add_plotting_col()
215 id="offcanvas-documentation",
216 title="Documentation",
219 children=html.Iframe(
220 src=C.URL_DOC_REL_NOTES,
240 def _add_ctrl_col(self) -> dbc.Col:
241 """Add column with controls. It is placed on the left side.
243 :returns: Column with the control panel.
248 children=self._add_ctrl_panel(),
249 className="sticky-top"
253 def _add_plotting_col(self) -> dbc.Col:
254 """Add column with plots. It is placed on the right side.
256 :returns: Column with plots.
260 id="col-plotting-area",
266 class_name="g-0 p-0",
277 def _add_ctrl_panel(self) -> list:
278 """Add control panel.
280 :returns: Control panel.
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...",
296 {"label": k, "value": k} \
297 for k in self._tbs.keys()
299 key=lambda d: d["label"]
308 class_name="g-0 p-1",
312 dbc.InputGroupText("CSIT and DUT Version"),
314 id={"type": "ctrl-dd", "index": "dutver"},
315 placeholder="Select a CSIT and DUT Version...")
322 class_name="g-0 p-1",
326 dbc.InputGroupText("Infra"),
328 id={"type": "ctrl-dd", "index": "infra"},
330 "Select a Physical Test Bed Topology..."
338 class_name="g-0 p-1",
342 dbc.InputGroupText("Frame Size"),
344 id={"type": "ctrl-cl", "index": "frmsize"},
349 style={"align-items": "center"},
355 class_name="g-0 p-1",
359 dbc.InputGroupText("Number of Cores"),
361 id={"type": "ctrl-cl", "index": "core"},
366 style={"align-items": "center"},
372 class_name="g-0 p-1",
376 dbc.InputGroupText("Measurement"),
378 id={"type": "ctrl-cl", "index": "ttype"},
383 style={"align-items": "center"},
392 class_name="g-0 p-1",
396 dbc.InputGroupText("Parameter"),
398 id={"type": "ctrl-dd", "index": "cmpprm"},
399 placeholder="Select a Parameter..."
407 class_name="g-0 p-1",
411 dbc.InputGroupText("Value"),
413 id={"type": "ctrl-dd", "index": "cmpval"},
414 placeholder="Select a Value..."
425 class_name="g-0 p-1",
431 "value": "normalize",
432 "label": "Normalize to 2GHz CPU frequency"
438 style={"align-items": "center"},
450 html.H5("Reference Value")
466 html.H5("Compared Value")
482 html.H5("Normalization")
497 def _get_plotting_area(
502 """Generate the plotting area with all its content.
504 :param title: The title of the comparison table.
505 :param table: Comparison table to be displayed.
506 :param url: URL to be displayed in the modal window.
508 :type table: pandas.DataFrame
510 :returns: List of rows with elements to be displayed in the plotting
519 "No data for comparison.",
522 class_name="g-0 p-1",
528 for idx, col in enumerate(table.columns):
538 l_col = col.rsplit(" ", 2)
540 "name": [l_col[0], " ".join(l_col[-2:])],
545 "format": Format(precision=2, scheme=Scheme.fixed)
550 children=html.H5(title),
556 children=dash_table.DataTable(
557 id={"type": "table", "index": "comparison"},
559 data=table.to_dict("records"),
560 merge_duplicate_headers=True,
562 filter_action="custom",
564 sort_action="native",
569 style_cell={"textAlign": "right"},
570 style_cell_conditional=[{
571 "if": {"column_id": "Test Name"},
590 "text-transform": "none",
591 "padding": "0rem 1rem"
596 dbc.ModalHeader(dbc.ModalTitle("URL")),
605 id="plot-btn-download",
606 children="Download Data",
610 "text-transform": "none",
611 "padding": "0rem 1rem"
614 dcc.Download(id="download-iterative-data")
617 "d-grid gap-0 d-md-flex justify-content-md-end"
623 children=C.PLACEHOLDER,
628 def callbacks(self, app):
629 """Callbacks for the whole application.
631 :param app: The application.
637 Output("store-control-panel", "data"),
638 Output("store-selected", "data"),
639 Output("store-table-data", "data"),
640 Output("store-filtered-table-data", "data"),
641 Output("plotting-area", "children"),
642 Output({"type": "table", "index": ALL}, "data"),
643 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
644 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
645 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
646 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
647 Output({"type": "ctrl-dd", "index": "infra"}, "options"),
648 Output({"type": "ctrl-dd", "index": "infra"}, "disabled"),
649 Output({"type": "ctrl-dd", "index": "infra"}, "value"),
650 Output({"type": "ctrl-cl", "index": "core"}, "options"),
651 Output({"type": "ctrl-cl", "index": "core"}, "value"),
652 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
653 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
654 Output({"type": "ctrl-cl", "index": "ttype"}, "options"),
655 Output({"type": "ctrl-cl", "index": "ttype"}, "value"),
656 Output({"type": "ctrl-dd", "index": "cmpprm"}, "options"),
657 Output({"type": "ctrl-dd", "index": "cmpprm"}, "disabled"),
658 Output({"type": "ctrl-dd", "index": "cmpprm"}, "value"),
659 Output({"type": "ctrl-dd", "index": "cmpval"}, "options"),
660 Output({"type": "ctrl-dd", "index": "cmpval"}, "disabled"),
661 Output({"type": "ctrl-dd", "index": "cmpval"}, "value"),
662 Output("normalize", "value")
665 State("store-control-panel", "data"),
666 State("store-selected", "data"),
667 State("store-table-data", "data"),
668 State("store-filtered-table-data", "data"),
669 State({"type": "table", "index": ALL}, "data")
672 Input("url", "href"),
673 Input("normalize", "value"),
674 Input({"type": "table", "index": ALL}, "filter_query"),
675 Input({"type": "ctrl-dd", "index": ALL}, "value"),
676 Input({"type": "ctrl-cl", "index": ALL}, "value"),
677 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
680 def _update_application(
683 store_table_data: list,
691 """Update the application when the event is detected.
694 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
707 parsed_url = url_decode(href)
709 url_params = parsed_url["params"]
714 plotting_area = no_update
716 trigger = Trigger(callback_context.triggered)
717 if trigger.type == "url" and url_params:
720 selected = literal_eval(url_params["selected"][0])
721 r_sel = selected["reference"]["selection"]
722 c_sel = selected["compare"]
723 normalize = literal_eval(url_params["norm"][0])
725 (selected["reference"]["set"] == True) and
726 (c_sel["set"] == True)
728 except (KeyError, IndexError, AttributeError):
732 "dut-val": r_sel["dut"],
733 "dutver-opt": generate_options(
734 self._tbs[r_sel["dut"]].keys()
737 "dutver-val": r_sel["dutver"],
738 "infra-opt": generate_options(
739 self._tbs[r_sel["dut"]][r_sel["dutver"]].keys()
742 "infra-val": r_sel["infra"],
743 "core-opt": generate_options(
744 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
745 [r_sel["infra"]]["core"]
747 "core-val": r_sel["core"],
748 "frmsize-opt": generate_options(
749 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
750 [r_sel["infra"]]["fsize"]
752 "frmsize-val": r_sel["frmsize"],
753 "ttype-opt": generate_options(
754 self._tbs[r_sel["dut"]][r_sel["dutver"]]\
755 [r_sel["infra"]]["ttype"]
757 "ttype-val": r_sel["ttype"],
758 "normalize-val": normalize
761 for itm, label in CMP_PARAMS.items():
762 if len(ctrl_panel.get(f"{itm}-opt")) > 1:
763 opts.append({"label": label, "value": itm})
766 "cmp-par-dis": False,
767 "cmp-par-val": c_sel["parameter"]
770 for itm in ctrl_panel.get(f"{c_sel['parameter']}-opt"):
771 set_val = ctrl_panel.get(f"{c_sel['parameter']}-val")
772 if isinstance(set_val, list):
773 if itm["value"] not in set_val:
776 if itm["value"] != set_val:
780 "cmp-val-dis": False,
781 "cmp-val-val": c_sel["value"]
784 elif trigger.type == "normalize":
785 ctrl_panel.set({"normalize-val": normalize})
787 elif trigger.type == "ctrl-dd":
788 if trigger.idx == "dut":
790 opts = generate_options(self._tbs[trigger.value].keys())
796 "dut-val": trigger.value,
798 "dutver-dis": disabled,
805 "frmsize-opt": list(),
806 "frmsize-val": list(),
809 "cmp-par-opt": list(),
811 "cmp-par-val": str(),
812 "cmp-val-opt": list(),
816 elif trigger.idx == "dutver":
818 dut = ctrl_panel.get("dut-val")
819 dver = self._tbs[dut][trigger.value]
820 opts = generate_options(dver.keys())
826 "dutver-val": trigger.value,
828 "infra-dis": disabled,
832 "frmsize-opt": list(),
833 "frmsize-val": list(),
836 "cmp-par-opt": list(),
838 "cmp-par-val": str(),
839 "cmp-val-opt": list(),
843 elif trigger.idx == "infra":
844 dut = ctrl_panel.get("dut-val")
845 dver = ctrl_panel.get("dutver-val")
846 if all((dut, dver, trigger.value, )):
847 driver = self._tbs[dut][dver][trigger.value]
849 "infra-val": trigger.value,
850 "core-opt": generate_options(driver["core"]),
852 "frmsize-opt": generate_options(driver["fsize"]),
853 "frmsize-val": list(),
854 "ttype-opt": generate_options(driver["ttype"]),
856 "cmp-par-opt": list(),
858 "cmp-par-val": str(),
859 "cmp-val-opt": list(),
863 elif trigger.idx == "cmpprm":
864 value = trigger.value
866 for itm in ctrl_panel.get(f"{value}-opt"):
867 set_val = ctrl_panel.get(f"{value}-val")
868 if isinstance(set_val, list):
869 if itm["value"] == "Latency":
871 if itm["value"] not in set_val:
874 if itm["value"] != set_val:
877 "cmp-par-val": value,
879 "cmp-val-dis": False,
882 elif trigger.idx == "cmpval":
883 ctrl_panel.set({"cmp-val-val": trigger.value})
884 selected["reference"] = {
887 "dut": ctrl_panel.get("dut-val"),
888 "dutver": ctrl_panel.get("dutver-val"),
889 "infra": ctrl_panel.get("infra-val"),
890 "core": ctrl_panel.get("core-val"),
891 "frmsize": ctrl_panel.get("frmsize-val"),
892 "ttype": ctrl_panel.get("ttype-val")
895 selected["compare"] = {
897 "parameter": ctrl_panel.get("cmp-par-val"),
898 "value": trigger.value
901 elif trigger.type == "ctrl-cl":
902 ctrl_panel.set({f"{trigger.idx}-val": trigger.value})
903 if all((ctrl_panel.get("core-val"),
904 ctrl_panel.get("frmsize-val"),
905 ctrl_panel.get("ttype-val"), )):
906 if "Latency" in ctrl_panel.get("ttype-val"):
907 ctrl_panel.set({"ttype-val": ["Latency", ]})
909 for itm, label in CMP_PARAMS.items():
910 if "Latency" in ctrl_panel.get("ttype-val") and \
913 if len(ctrl_panel.get(f"{itm}-opt")) > 1:
914 if isinstance(ctrl_panel.get(f"{itm}-val"), list):
915 if len(ctrl_panel.get(f"{itm}-opt")) == \
916 len(ctrl_panel.get(f"{itm}-val")):
918 opts.append({"label": label, "value": itm})
921 "cmp-par-dis": False,
922 "cmp-par-val": str(),
923 "cmp-val-opt": list(),
929 "cmp-par-opt": list(),
931 "cmp-par-val": str(),
932 "cmp-val-opt": list(),
936 elif trigger.type == "table" and trigger.idx == "comparison":
937 filtered_data = filter_table_data(
941 table_data = [filtered_data, ]
943 if all((on_draw, selected["reference"]["set"],
944 selected["compare"]["set"], )):
945 title, table = comparison_table(self._data, selected, normalize)
946 plotting_area = self._get_plotting_area(
951 params={"selected": selected, "norm": normalize}
954 store_table_data = table.to_dict("records")
955 filtered_data = store_table_data
957 table_data = [store_table_data, ]
967 ret_val.extend(ctrl_panel.values)
971 Output("plot-mod-url", "is_open"),
972 Input("plot-btn-url", "n_clicks"),
973 State("plot-mod-url", "is_open")
975 def toggle_plot_mod_url(n, is_open):
976 """Toggle the modal window with url.
983 Output("download-iterative-data", "data"),
984 State("store-table-data", "data"),
985 State("store-filtered-table-data", "data"),
986 Input("plot-btn-download", "n_clicks"),
987 prevent_initial_call=True
989 def _download_comparison_data(
991 filtered_table_data: list,
994 """Download the data.
996 :param table_data: Original unfiltered table data.
997 :param filtered_table_data: Filtered table data.
998 :type table_data: list
999 :type filtered_table_data: list
1000 :returns: dict of data frame content (base64 encoded) and meta data
1001 used by the Download component.
1008 if filtered_table_data:
1009 table = pd.DataFrame.from_records(filtered_table_data)
1011 table = pd.DataFrame.from_records(table_data)
1013 return dcc.send_data_frame(table.to_csv, C.COMP_DOWNLOAD_FILE_NAME)
1016 Output("offcanvas-documentation", "is_open"),
1017 Input("btn-documentation", "n_clicks"),
1018 State("offcanvas-documentation", "is_open")
1020 def toggle_offcanvas_documentation(n_clicks, is_open):