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.
15 """Plotly Dash HTML layout override.
20 import dash_bootstrap_components as dbc
22 from flask import Flask
25 from dash import callback_context, no_update, ALL
26 from dash import Input, Output, State
27 from dash.exceptions import PreventUpdate
28 from yaml import load, FullLoader, YAMLError
29 from ast import literal_eval
30 from copy import deepcopy
32 from ..utils.constants import Constants as C
33 from ..utils.control_panel import ControlPanel
34 from ..utils.trigger import Trigger
35 from ..utils.telemetry_data import TelemetryData
36 from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
37 generate_options, get_list_group_items, graph_hdrh_latency
38 from ..utils.url_processing import url_decode
39 from .graphs import graph_trending, select_trending_data, graph_tm_trending
42 # Control panel partameters and their default values.
48 "dd-area-opt": list(),
51 "dd-test-opt": list(),
54 "cl-core-opt": list(),
55 "cl-core-val": list(),
56 "cl-core-all-val": list(),
57 "cl-core-all-opt": C.CL_ALL_DISABLED,
58 "cl-frmsize-opt": list(),
59 "cl-frmsize-val": list(),
60 "cl-frmsize-all-val": list(),
61 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
62 "cl-tsttype-opt": list(),
63 "cl-tsttype-val": list(),
64 "cl-tsttype-all-val": list(),
65 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
67 "cl-normalize-val": list()
72 """The layout of the dash app and the callbacks.
77 data_trending: pd.DataFrame,
78 html_layout_file: str,
79 graph_layout_file: str,
83 - save the input parameters,
84 - read and pre-process the data,
85 - prepare data for the control panel,
86 - read HTML layout file,
87 - read tooltips from the tooltip file.
89 :param app: Flask application running the dash application.
90 :param data_trending: Pandas dataframe with trending data.
91 :param html_layout_file: Path and name of the file specifying the HTML
92 layout of the dash application.
93 :param graph_layout_file: Path and name of the file with layout of
95 :param tooltip_file: Path and name of the yaml file specifying the
98 :type data_trending: pandas.DataFrame
99 :type html_layout_file: str
100 :type graph_layout_file: str
101 :type tooltip_file: str
106 self._data = data_trending
107 self._html_layout_file = html_layout_file
108 self._graph_layout_file = graph_layout_file
109 self._tooltip_file = tooltip_file
111 # Get structure of tests:
113 cols = ["job", "test_id", "test_type", "tg_type"]
114 for _, row in self._data[cols].drop_duplicates().iterrows():
115 lst_job = row["job"].split("-")
117 tbed = "-".join(lst_job[-2:])
118 lst_test = row["test_id"].split(".")
122 area = ".".join(lst_test[3:-2])
123 suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\
126 nic = suite.split("-")[0]
127 for drv in C.DRIVERS:
133 test = test.replace(f"{drv}-", "")
137 infra = "-".join((tbed, nic, driver))
138 lst_test = test.split("-")
139 framesize = lst_test[0]
140 core = lst_test[1] if lst_test[1] else "8C"
141 test = "-".join(lst_test[2: -1])
143 if tbs.get(dut, None) is None:
145 if tbs[dut].get(area, None) is None:
146 tbs[dut][area] = dict()
147 if tbs[dut][area].get(test, None) is None:
148 tbs[dut][area][test] = dict()
149 if tbs[dut][area][test].get(infra, None) is None:
150 tbs[dut][area][test][infra] = {
152 "frame-size": list(),
155 tst_params = tbs[dut][area][test][infra]
156 if core.upper() not in tst_params["core"]:
157 tst_params["core"].append(core.upper())
158 if framesize.upper() not in tst_params["frame-size"]:
159 tst_params["frame-size"].append(framesize.upper())
160 if row["test_type"] == "mrr":
161 if "MRR" not in tst_params["test-type"]:
162 tst_params["test-type"].append("MRR")
163 elif row["test_type"] == "ndrpdr":
164 if "NDR" not in tst_params["test-type"]:
165 tst_params["test-type"].extend(("NDR", "PDR"))
166 elif row["test_type"] == "hoststack":
167 if row["tg_type"] in ("iperf", "vpp"):
168 if "BPS" not in tst_params["test-type"]:
169 tst_params["test-type"].append("BPS")
170 elif row["tg_type"] == "ab":
171 if "CPS" not in tst_params["test-type"]:
172 tst_params["test-type"].extend(("CPS", "RPS"))
176 self._html_layout = str()
177 self._graph_layout = None
178 self._tooltips = dict()
181 with open(self._html_layout_file, "r") as file_read:
182 self._html_layout = file_read.read()
183 except IOError as err:
185 f"Not possible to open the file {self._html_layout_file}\n{err}"
189 with open(self._graph_layout_file, "r") as file_read:
190 self._graph_layout = load(file_read, Loader=FullLoader)
191 except IOError as err:
193 f"Not possible to open the file {self._graph_layout_file}\n"
196 except YAMLError as err:
198 f"An error occurred while parsing the specification file "
199 f"{self._graph_layout_file}\n{err}"
203 with open(self._tooltip_file, "r") as file_read:
204 self._tooltips = load(file_read, Loader=FullLoader)
205 except IOError as err:
207 f"Not possible to open the file {self._tooltip_file}\n{err}"
209 except YAMLError as err:
211 f"An error occurred while parsing the specification file "
212 f"{self._tooltip_file}\n{err}"
216 if self._app is not None and hasattr(self, "callbacks"):
217 self.callbacks(self._app)
220 def html_layout(self):
221 return self._html_layout
223 def add_content(self):
224 """Top level method which generated the web page.
227 - Store for user input data,
229 - Main area with control panel and ploting area.
231 If no HTML layout is provided, an error message is displayed instead.
233 :returns: The HTML div with the whole page.
237 if self.html_layout and self._spec_tbs:
242 dcc.Store(id="store"),
243 dcc.Location(id="url", refresh=False),
255 self._add_ctrl_col(),
256 self._add_plotting_col()
262 id="offcanvas-metadata",
263 title="Detailed Information",
267 dbc.Row(id="metadata-tput-lat"),
268 dbc.Row(id="metadata-hdrh-graph")
271 delay_show=C.SPINNER_DELAY
275 id="offcanvas-documentation",
276 title="Documentation",
279 children=html.Iframe(
280 src=C.URL_DOC_TRENDING,
289 dbc.Alert("An Error Occured", color="danger"),
293 def _add_navbar(self):
294 """Add nav element with navigation panel. It is placed on the top.
296 :returns: Navigation bar.
297 :rtype: dbc.NavbarSimple
299 return dbc.NavbarSimple(
301 dbc.NavItem(dbc.NavLink(
307 dbc.NavItem(dbc.NavLink(
312 dbc.NavItem(dbc.NavLink(
317 dbc.NavItem(dbc.NavLink(
319 id="btn-documentation",
322 id="navbarsimple-main",
325 brand_external_link=True,
330 def _add_ctrl_col(self) -> dbc.Col:
331 """Add column with controls. It is placed on the left side.
333 :returns: Column with the control panel.
336 return dbc.Col(html.Div(self._add_ctrl_panel(), className="sticky-top"))
338 def _add_ctrl_panel(self) -> list:
339 """Add control panel.
341 :returns: Control panel.
349 show_tooltip(self._tooltips, "help-dut", "DUT")
352 id={"type": "ctrl-dd", "index": "dut"},
353 placeholder="Select a Device under Test...",
356 {"label": k, "value": k} \
357 for k in self._spec_tbs.keys()
359 key=lambda d: d["label"]
371 show_tooltip(self._tooltips, "help-area", "Area")
374 id={"type": "ctrl-dd", "index": "area"},
375 placeholder="Select an Area..."
386 show_tooltip(self._tooltips, "help-test", "Test")
389 id={"type": "ctrl-dd", "index": "test"},
390 placeholder="Select a Test..."
401 show_tooltip(self._tooltips, "help-infra", "Infra")
404 id={"type": "ctrl-dd", "index": "phy"},
405 placeholder="Select a Physical Test Bed Topology..."
415 dbc.InputGroupText(show_tooltip(
422 id={"type": "ctrl-cl", "index": "frmsize-all"},
423 options=C.CL_ALL_DISABLED,
431 id={"type": "ctrl-cl", "index": "frmsize"},
436 style={"align-items": "center"},
444 dbc.InputGroupText(show_tooltip(
451 id={"type": "ctrl-cl", "index": "core-all"},
452 options=C.CL_ALL_DISABLED,
460 id={"type": "ctrl-cl", "index": "core"},
465 style={"align-items": "center"},
473 dbc.InputGroupText(show_tooltip(
480 id={"type": "ctrl-cl", "index": "tsttype-all"},
481 options=C.CL_ALL_DISABLED,
489 id={"type": "ctrl-cl", "index": "tsttype"},
494 style={"align-items": "center"},
502 dbc.InputGroupText(show_tooltip(
507 dbc.Col(dbc.Checklist(
510 "value": "normalize",
511 "label": "Normalize to CPU frequency 2GHz"
518 style={"align-items": "center"},
525 id={"type": "ctrl-btn", "index": "add-test"},
526 children="Add Selected",
533 class_name="overflow-auto p-0",
536 style={"max-height": "20em"},
539 id="row-card-sel-tests",
540 class_name="g-0 p-1",
541 style=C.STYLE_DISABLED,
547 id={"type": "ctrl-btn", "index": "rm-test"},
554 id={"type": "ctrl-btn", "index": "rm-test-all"},
560 id="row-btns-sel-tests",
561 class_name="g-0 p-1",
562 style=C.STYLE_DISABLED,
567 "Add Telemetry Panel",
568 id={"type": "telemetry-btn", "index": "open"},
571 dbc.Button("Show URL", id="plot-btn-url", color="info"),
574 dbc.ModalHeader(dbc.ModalTitle("URL")),
575 dbc.ModalBody(id="mod-url")
583 id="row-btns-add-tm",
584 class_name="g-0 p-1",
585 style=C.STYLE_DISABLED,
590 def _add_plotting_col(self) -> dbc.Col:
591 """Add column with plots. It is placed on the right side.
593 :returns: Column with plots.
597 id="col-plotting-area",
600 id="plotting-area-trending",
601 class_name="g-0 p-0",
602 children=C.PLACEHOLDER
605 id="plotting-area-telemetry",
606 class_name="g-0 p-0",
607 children=C.PLACEHOLDER
611 style=C.STYLE_DISABLED,
615 def _plotting_area_trending(graphs: list) -> dbc.Col:
616 """Generate the plotting area with all its content.
618 :param graphs: A list of graphs to be displayed in the trending page.
620 :returns: A collumn with trending graphs (tput and latency) in tabs.
632 id={"type": "graph", "index": "tput"},
644 id={"type": "graph", "index": "bandwidth"},
648 tab_id="tab-bandwidth"
656 id={"type": "graph", "index": "lat"},
669 active_tab="tab-tput",
678 id="plot-btn-download",
681 style={"padding": "0rem 1rem"}
683 dcc.Download(id="download-trending-data")
685 className="d-grid gap-0 d-md-flex justify-content-md-end"
694 dbc.AccordionItem(trending, title="Trending"),
695 class_name="g-0 p-1",
696 start_collapsed=False,
698 active_item=["item-0", ]
703 dbc.ModalTitle("Select a Metric"),
707 dbc.ModalBody(Layout._get_telemetry_step_1()),
708 delay_show=2 * C.SPINNER_DELAY
713 id={"type": "telemetry-btn", "index": "select"},
719 id={"type": "telemetry-btn", "index": "cancel"},
725 id={"type": "telemetry-btn", "index": "rm-all"},
731 id={"type": "plot-mod-telemetry", "index": 0},
741 dbc.ModalTitle("Select Labels"),
745 dbc.ModalBody(Layout._get_telemetry_step_2()),
746 delay_show=2 * C.SPINNER_DELAY
751 id={"type": "telemetry-btn", "index": "back"},
756 "Add Telemetry Panel",
757 id={"type": "telemetry-btn", "index": "add"},
763 id={"type": "telemetry-btn", "index": "cancel"},
769 id={"type": "plot-mod-telemetry", "index": 1},
780 def _plotting_area_telemetry(graphs: list) -> dbc.Col:
781 """Generate the plotting area with telemetry.
783 :param graphs: A list of graphs to be displayed in the telemetry page.
785 :returns: A collumn with telemetry trending graphs.
791 def _plural(iterative):
792 return "s" if len(iterative) > 1 else str()
795 for idx, graph_set in enumerate(graphs):
797 for graph in graph_set[0]:
798 graph_name = ", ".join(graph[1])
802 id={"type": "graph-telemetry", "index": graph_name},
805 title=(f"Test{_plural(graph[1])}: {graph_name}"),
815 class_name="g-0 p-0",
816 start_collapsed=True,
828 "type": "tm-btn-remove",
833 style={"padding": "0rem 1rem"}
838 "type": "tm-btn-download",
843 style={"padding": "0rem 1rem"}
847 "d-grid gap-0 d-md-flex justify-content-md-end"
852 class_name="g-0 p-0",
854 f"Metric{_plural(graph_set[1])}: ",
855 ", ".join(graph_set[1])
863 class_name="g-0 p-1",
864 start_collapsed=True,
870 def _get_telemetry_step_1() -> list:
871 """Return the content of the modal window used in the step 1 of metrics
874 :returns: A list of dbc rows with 'input' and 'search output'.
879 class_name="g-0 p-1",
882 id={"type": "telemetry-search-in", "index": 0},
883 placeholder="Start typing a metric name...",
889 class_name="g-0 p-1",
892 class_name="overflow-auto p-0",
893 id={"type": "telemetry-search-out", "index": 0},
895 style={"max-height": "14em"},
903 def _get_telemetry_step_2() -> list:
904 """Return the content of the modal window used in the step 2 of metrics
907 :returns: A list of dbc rows with 'container with dynamic dropdowns' and
914 id={"type": "tm-container", "index": 0},
921 id={"type": "cb-all-in-one", "index": 0},
922 label="All Metrics in one Graph"
928 id={"type": "cb-ignore-host", "index": 0},
938 id={"type": "tm-list-metrics", "index": 0},
948 def callbacks(self, app):
949 """Callbacks for the whole application.
951 :param app: The application.
956 Output("store", "data"),
957 Output("plotting-area-trending", "children"),
958 Output("plotting-area-telemetry", "children"),
959 Output("col-plotting-area", "style"),
960 Output("row-card-sel-tests", "style"),
961 Output("row-btns-sel-tests", "style"),
962 Output("row-btns-add-tm", "style"),
963 Output("lg-selected", "children"),
964 Output({"type": "telemetry-search-out", "index": ALL}, "children"),
965 Output({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
966 Output({"type": "telemetry-btn", "index": ALL}, "disabled"),
967 Output({"type": "tm-container", "index": ALL}, "children"),
968 Output({"type": "tm-list-metrics", "index": ALL}, "value"),
969 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
970 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
971 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
972 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
973 Output({"type": "ctrl-dd", "index": "area"}, "options"),
974 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
975 Output({"type": "ctrl-dd", "index": "area"}, "value"),
976 Output({"type": "ctrl-dd", "index": "test"}, "options"),
977 Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
978 Output({"type": "ctrl-dd", "index": "test"}, "value"),
979 Output({"type": "ctrl-cl", "index": "core"}, "options"),
980 Output({"type": "ctrl-cl", "index": "core"}, "value"),
981 Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
982 Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
983 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
984 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
985 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
986 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
987 Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
988 Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
989 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
990 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
991 Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
992 Output("normalize", "value"),
994 State("store", "data"),
995 State({"type": "sel-cl", "index": ALL}, "value"),
996 State({"type": "cb-all-in-one", "index": ALL}, "value"),
997 State({"type": "cb-ignore-host", "index": ALL}, "value"),
998 State({"type": "telemetry-search-out", "index": ALL}, "children"),
999 State({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
1000 State({"type": "telemetry-btn", "index": ALL}, "disabled"),
1001 State({"type": "tm-container", "index": ALL}, "children"),
1002 State({"type": "tm-list-metrics", "index": ALL}, "value"),
1003 State({"type": "tele-cl", "index": ALL}, "value"),
1005 Input("url", "href"),
1006 Input({"type": "tm-dd", "index": ALL}, "value"),
1008 Input("normalize", "value"),
1009 Input({"type": "telemetry-search-in", "index": ALL}, "value"),
1010 Input({"type": "telemetry-btn", "index": ALL}, "n_clicks"),
1011 Input({"type": "tm-btn-remove", "index": ALL}, "n_clicks"),
1012 Input({"type": "ctrl-dd", "index": ALL}, "value"),
1013 Input({"type": "ctrl-cl", "index": ALL}, "value"),
1014 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks"),
1016 prevent_initial_call=True
1018 def _update_application(
1025 tm_btns_disabled: list,
1033 """Update the application when the event is detected.
1038 "control-panel": dict(),
1039 "selected-tests": list(),
1040 "trending-graphs": None,
1041 "telemetry-data": dict(),
1042 "selected-metrics": dict(),
1043 "telemetry-panels": list(),
1044 "telemetry-all-in-one": list(),
1045 "telemetry-ignore-host": list(),
1046 "telemetry-graphs": list(),
1050 ctrl_panel = ControlPanel(
1052 store.get("control-panel", dict())
1054 store_sel = store["selected-tests"]
1055 tm_data = store["telemetry-data"]
1056 tm_user = store["selected-metrics"]
1057 tm_panels = store["telemetry-panels"]
1058 tm_all_in_one = store["telemetry-all-in-one"]
1059 tm_ignore_host = store["telemetry-ignore-host"]
1061 plotting_area_telemetry = no_update
1062 on_draw = [False, False] # 0 --> trending, 1 --> telemetry
1065 parsed_url = url_decode(href)
1067 url_params = parsed_url["params"]
1072 # Telemetry user data
1073 # The data provided by user or result of user action
1075 # List of unique metrics:
1076 "unique_metrics": list(),
1077 # List of metrics selected by user:
1078 "selected_metrics": list(),
1079 # Labels from metrics selected by user (key: label name,
1080 # value: list of all possible values):
1081 "unique_labels": dict(),
1082 # Labels selected by the user (subset of 'unique_labels'):
1083 "selected_labels": dict(),
1084 # All unique metrics with labels (output from the step 1)
1085 # converted from pandas dataframe to dictionary.
1086 "unique_metrics_with_labels": dict(),
1087 # Metrics with labels selected by the user using dropdowns.
1088 "selected_metrics_with_labels": dict()
1090 tm = TelemetryData(store_sel) if store_sel else TelemetryData()
1092 trigger = Trigger(callback_context.triggered)
1093 if trigger.type == "url" and url_params:
1096 store_sel = literal_eval(url_params["store_sel"][0])
1097 normalize = literal_eval(url_params["norm"][0])
1098 telemetry = literal_eval(url_params["telemetry"][0])
1099 url_p = url_params.get("all-in-one", ["[[None]]"])
1100 tm_all_in_one = literal_eval(url_p[0])
1101 url_p = url_params.get("ignore-host", ["[[None]]"])
1102 tm_ignore_host = literal_eval(url_p[0])
1103 if not isinstance(telemetry, list):
1104 telemetry = [telemetry, ]
1105 except (KeyError, IndexError, AttributeError, ValueError):
1108 last_test = store_sel[-1]
1109 test = self._spec_tbs[last_test["dut"]]\
1110 [last_test["area"]][last_test["test"]][last_test["phy"]]
1112 "dd-dut-val": last_test["dut"],
1113 "dd-area-val": last_test["area"],
1115 {"label": label(v), "value": v} for v in sorted(
1116 self._spec_tbs[last_test["dut"]].keys())
1118 "dd-area-dis": False,
1119 "dd-test-val": last_test["test"],
1120 "dd-test-opt": generate_options(
1121 self._spec_tbs[last_test["dut"]]\
1122 [last_test["area"]].keys()
1124 "dd-test-dis": False,
1125 "dd-phy-val": last_test["phy"],
1126 "dd-phy-opt": generate_options(
1127 self._spec_tbs[last_test["dut"]][last_test["area"]]\
1128 [last_test["test"]].keys()
1130 "dd-phy-dis": False,
1131 "cl-core-opt": generate_options(test["core"]),
1132 "cl-core-val": [last_test["core"].upper(), ],
1133 "cl-core-all-val": list(),
1134 "cl-core-all-opt": C.CL_ALL_ENABLED,
1135 "cl-frmsize-opt": generate_options(test["frame-size"]),
1136 "cl-frmsize-val": [last_test["framesize"].upper(), ],
1137 "cl-frmsize-all-val": list(),
1138 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1139 "cl-tsttype-opt": generate_options(test["test-type"]),
1140 "cl-tsttype-val": [last_test["testtype"].upper(), ],
1141 "cl-tsttype-all-val": list(),
1142 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1143 "cl-normalize-val": normalize,
1144 "btn-add-dis": False
1146 store["trending-graphs"] = None
1147 store["telemetry-graphs"] = list()
1150 tm = TelemetryData(store_sel)
1151 tm.from_dataframe(self._data)
1152 tm_data = tm.to_json()
1153 tm.from_json(tm_data)
1154 tm_panels = telemetry
1156 elif trigger.type == "normalize":
1157 ctrl_panel.set({"cl-normalize-val": trigger.value})
1158 store["trending-graphs"] = None
1160 elif trigger.type == "ctrl-dd":
1161 if trigger.idx == "dut":
1163 dut = self._spec_tbs[trigger.value]
1164 options = [{"label": label(v), "value": v} \
1165 for v in sorted(dut.keys())]
1171 "dd-dut-val": trigger.value,
1172 "dd-area-val": str(),
1173 "dd-area-opt": options,
1174 "dd-area-dis": disabled,
1175 "dd-test-val": str(),
1176 "dd-test-opt": list(),
1177 "dd-test-dis": True,
1178 "dd-phy-val": str(),
1179 "dd-phy-opt": list(),
1181 "cl-core-opt": list(),
1182 "cl-core-val": list(),
1183 "cl-core-all-val": list(),
1184 "cl-core-all-opt": C.CL_ALL_DISABLED,
1185 "cl-frmsize-opt": list(),
1186 "cl-frmsize-val": list(),
1187 "cl-frmsize-all-val": list(),
1188 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1189 "cl-tsttype-opt": list(),
1190 "cl-tsttype-val": list(),
1191 "cl-tsttype-all-val": list(),
1192 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1195 if trigger.idx == "area":
1197 dut = ctrl_panel.get("dd-dut-val")
1198 area = self._spec_tbs[dut][trigger.value]
1199 options = generate_options(area.keys())
1205 "dd-area-val": trigger.value,
1206 "dd-test-val": str(),
1207 "dd-test-opt": options,
1208 "dd-test-dis": disabled,
1209 "dd-phy-val": str(),
1210 "dd-phy-opt": list(),
1212 "cl-core-opt": list(),
1213 "cl-core-val": list(),
1214 "cl-core-all-val": list(),
1215 "cl-core-all-opt": C.CL_ALL_DISABLED,
1216 "cl-frmsize-opt": list(),
1217 "cl-frmsize-val": list(),
1218 "cl-frmsize-all-val": list(),
1219 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1220 "cl-tsttype-opt": list(),
1221 "cl-tsttype-val": list(),
1222 "cl-tsttype-all-val": list(),
1223 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1226 if trigger.idx == "test":
1228 dut = ctrl_panel.get("dd-dut-val")
1229 area = ctrl_panel.get("dd-area-val")
1230 test = self._spec_tbs[dut][area][trigger.value]
1231 options = generate_options(test.keys())
1237 "dd-test-val": trigger.value,
1238 "dd-phy-val": str(),
1239 "dd-phy-opt": options,
1240 "dd-phy-dis": disabled,
1241 "cl-core-opt": list(),
1242 "cl-core-val": list(),
1243 "cl-core-all-val": list(),
1244 "cl-core-all-opt": C.CL_ALL_DISABLED,
1245 "cl-frmsize-opt": list(),
1246 "cl-frmsize-val": list(),
1247 "cl-frmsize-all-val": list(),
1248 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1249 "cl-tsttype-opt": list(),
1250 "cl-tsttype-val": list(),
1251 "cl-tsttype-all-val": list(),
1252 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1255 if trigger.idx == "phy":
1256 dut = ctrl_panel.get("dd-dut-val")
1257 area = ctrl_panel.get("dd-area-val")
1258 test = ctrl_panel.get("dd-test-val")
1259 if all((dut, area, test, trigger.value, )):
1260 phy = self._spec_tbs[dut][area][test][trigger.value]
1262 "dd-phy-val": trigger.value,
1263 "cl-core-opt": generate_options(phy["core"]),
1264 "cl-core-val": list(),
1265 "cl-core-all-val": list(),
1266 "cl-core-all-opt": C.CL_ALL_ENABLED,
1268 generate_options(phy["frame-size"]),
1269 "cl-frmsize-val": list(),
1270 "cl-frmsize-all-val": list(),
1271 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1273 generate_options(phy["test-type"]),
1274 "cl-tsttype-val": list(),
1275 "cl-tsttype-all-val": list(),
1276 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1279 elif trigger.type == "ctrl-cl":
1280 param = trigger.idx.split("-")[0]
1281 if "-all" in trigger.idx:
1282 c_sel, c_all, c_id = list(), trigger.value, "all"
1284 c_sel, c_all, c_id = trigger.value, list(), str()
1285 val_sel, val_all = sync_checklists(
1286 options=ctrl_panel.get(f"cl-{param}-opt"),
1292 f"cl-{param}-val": val_sel,
1293 f"cl-{param}-all-val": val_all,
1295 if all((ctrl_panel.get("cl-core-val"),
1296 ctrl_panel.get("cl-frmsize-val"),
1297 ctrl_panel.get("cl-tsttype-val"), )):
1298 ctrl_panel.set({"btn-add-dis": False})
1300 ctrl_panel.set({"btn-add-dis": True})
1301 elif trigger.type == "ctrl-btn":
1303 tm_all_in_one = list()
1304 tm_ignore_host = list()
1305 store["trending-graphs"] = None
1306 store["telemetry-graphs"] = list()
1307 on_draw = [True, True]
1308 if trigger.idx == "add-test":
1309 dut = ctrl_panel.get("dd-dut-val")
1310 phy = ctrl_panel.get("dd-phy-val")
1311 area = ctrl_panel.get("dd-area-val")
1312 test = ctrl_panel.get("dd-test-val")
1313 # Add selected test(s) to the list of tests in store:
1314 if store_sel is None:
1316 for core in ctrl_panel.get("cl-core-val"):
1317 for framesize in ctrl_panel.get("cl-frmsize-val"):
1318 for ttype in ctrl_panel.get("cl-tsttype-val"):
1323 phy.replace('af_xdp', 'af-xdp'),
1330 if tid not in [i["id"] for i in store_sel]:
1337 "framesize": framesize.lower(),
1338 "core": core.lower(),
1339 "testtype": ttype.lower()
1341 store_sel = sorted(store_sel, key=lambda d: d["id"])
1342 if C.CLEAR_ALL_INPUTS:
1343 ctrl_panel.set(ctrl_panel.defaults)
1344 elif trigger.idx == "rm-test" and lst_sel:
1345 new_store_sel = list()
1346 for idx, item in enumerate(store_sel):
1347 if not lst_sel[idx]:
1348 new_store_sel.append(item)
1349 store_sel = new_store_sel
1350 elif trigger.idx == "rm-test-all":
1352 elif trigger.type == "telemetry-btn":
1353 if trigger.idx in ("open", "back"):
1354 tm.from_dataframe(self._data)
1355 tm_data = tm.to_json()
1356 tm_user["unique_metrics"] = tm.unique_metrics
1357 tm_user["selected_metrics"] = list()
1358 tm_user["unique_labels"] = dict()
1359 tm_user["selected_labels"] = dict()
1361 get_list_group_items(tm_user["unique_metrics"],
1364 is_open = (True, False)
1365 tm_btns_disabled[1], tm_btns_disabled[5] = False, True
1366 elif trigger.idx == "select":
1368 tm.from_json(tm_data)
1369 if not tm_user["selected_metrics"]:
1370 tm_user["selected_metrics"] = \
1371 tm_user["unique_metrics"]
1372 metrics = [a for a, b in \
1373 zip(tm_user["selected_metrics"], cl_metrics) if b]
1374 tm_user["selected_metrics"] = metrics
1375 tm_user["unique_labels"] = \
1376 tm.get_selected_labels(metrics)
1377 tm_user["unique_metrics_with_labels"] = \
1378 tm.unique_metrics_with_labels
1379 list_metrics[0] = tm.str_metrics
1380 tm_dd[0] = _get_dd_container(tm_user["unique_labels"])
1382 tm_btns_disabled[1] = True
1383 tm_btns_disabled[4] = False
1384 is_open = (False, True)
1386 is_open = (True, False)
1387 elif trigger.idx == "add":
1388 tm.from_json(tm_data)
1389 tm_panels.append(tm_user["selected_metrics_with_labels"])
1390 tm_all_in_one.append(all_in_one)
1391 tm_ignore_host.append(ignore_host)
1392 is_open = (False, False)
1393 tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1394 on_draw = [True, True]
1395 elif trigger.idx == "cancel":
1396 is_open = (False, False)
1397 tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1398 elif trigger.idx == "rm-all":
1400 tm_all_in_one = list()
1401 tm_ignore_host = list()
1403 is_open = (False, False)
1404 tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1405 plotting_area_telemetry = C.PLACEHOLDER
1406 elif trigger.type == "telemetry-search-in":
1407 tm.from_metrics(tm_user["unique_metrics"])
1408 tm_user["selected_metrics"] = \
1409 tm.search_unique_metrics(trigger.value)
1410 search_out = (get_list_group_items(
1411 tm_user["selected_metrics"],
1415 is_open = (True, False)
1416 elif trigger.type == "tm-dd":
1417 tm.from_metrics_with_labels(
1418 tm_user["unique_metrics_with_labels"]
1422 for itm in tm_dd_in:
1425 elif isinstance(itm, str):
1427 selected[itm] = list()
1428 elif isinstance(itm, list):
1429 if previous_itm is not None:
1430 selected[previous_itm] = itm
1433 tm_dd[0] = _get_dd_container(
1434 tm_user["unique_labels"],
1438 sel_metrics = tm.filter_selected_metrics_by_labels(selected)
1439 tm_user["selected_metrics_with_labels"] = sel_metrics.to_dict()
1440 if not sel_metrics.empty:
1441 list_metrics[0] = tm.metrics_to_str(sel_metrics)
1442 tm_btns_disabled[5] = False
1444 list_metrics[0] = str()
1445 elif trigger.type == "tm-btn-remove":
1446 del tm_panels[trigger.idx]
1447 del tm_all_in_one[trigger.idx]
1448 del tm_ignore_host[trigger.idx]
1449 del store["telemetry-graphs"][trigger.idx]
1450 tm.from_json(tm_data)
1451 on_draw = [True, True]
1454 "store_sel": store_sel,
1455 "norm": ctrl_panel.get("cl-normalize-val")
1458 new_url_params["telemetry"] = tm_panels
1459 new_url_params["all-in-one"] = tm_all_in_one
1460 new_url_params["ignore-host"] = tm_ignore_host
1462 if on_draw[0]: # Trending
1464 lg_selected = get_list_group_items(store_sel, "sel-cl")
1465 if store["trending-graphs"]:
1466 graphs = store["trending-graphs"]
1468 graphs = graph_trending(
1472 bool(ctrl_panel.get("cl-normalize-val"))
1474 if graphs and graphs[0]:
1475 store["trending-graphs"] = graphs
1476 plotting_area_trending = \
1477 Layout._plotting_area_trending(graphs)
1480 start_idx = len(store["telemetry-graphs"])
1481 end_idx = len(tm_panels)
1483 plotting_area_telemetry = C.PLACEHOLDER
1484 elif on_draw[1] and (end_idx >= start_idx):
1485 if len(tm_all_in_one) != end_idx:
1486 tm_all_in_one = [[None], ] * end_idx
1487 if len(tm_ignore_host) != end_idx:
1488 tm_ignore_host = [[None], ] * end_idx
1489 for idx in range(start_idx, end_idx):
1490 store["telemetry-graphs"].append(graph_tm_trending(
1491 tm.select_tm_trending_data(
1493 ignore_host=bool(tm_ignore_host[idx][0])
1496 bool(tm_all_in_one[idx][0])
1498 plotting_area_telemetry = \
1499 Layout._plotting_area_telemetry(
1500 store["telemetry-graphs"]
1502 col_plotting_area = C.STYLE_ENABLED
1503 row_card_sel_tests = C.STYLE_ENABLED
1504 row_btns_sel_tests = C.STYLE_ENABLED
1505 row_btns_add_tm = C.STYLE_ENABLED
1507 plotting_area_trending = no_update
1508 plotting_area_telemetry = C.PLACEHOLDER
1509 col_plotting_area = C.STYLE_DISABLED
1510 row_card_sel_tests = C.STYLE_DISABLED
1511 row_btns_sel_tests = C.STYLE_DISABLED
1512 row_btns_add_tm = C.STYLE_DISABLED
1513 lg_selected = no_update
1516 tm_all_in_one = list()
1517 tm_ignore_host = list()
1520 plotting_area_trending = no_update
1521 col_plotting_area = no_update
1522 row_card_sel_tests = no_update
1523 row_btns_sel_tests = no_update
1524 row_btns_add_tm = no_update
1525 lg_selected = no_update
1527 store["url"] = gen_new_url(parsed_url, new_url_params)
1528 store["control-panel"] = ctrl_panel.panel
1529 store["selected-tests"] = store_sel
1530 store["telemetry-data"] = tm_data
1531 store["selected-metrics"] = tm_user
1532 store["telemetry-panels"] = tm_panels
1533 store["telemetry-all-in-one"] = tm_all_in_one
1534 store["telemetry-ignore-host"] = tm_ignore_host
1537 plotting_area_trending,
1538 plotting_area_telemetry,
1550 ret_val.extend(ctrl_panel.values)
1554 Output("plot-mod-url", "is_open"),
1555 Output("mod-url", "children"),
1556 State("store", "data"),
1557 State("plot-mod-url", "is_open"),
1558 Input("plot-btn-url", "n_clicks")
1560 def toggle_plot_mod_url(store, is_open, n_clicks):
1561 """Toggle the modal window with url.
1567 return not is_open, store.get("url", str())
1568 return is_open, store["url"]
1570 def _get_dd_container(
1572 selected_labels: dict=dict(),
1575 """Generate a container with dropdown selection boxes depenting on
1578 :param all_labels: A dictionary with unique labels and their
1580 :param selected_labels: A dictionalry with user selected lables and
1582 :param show_new: If True, a dropdown selection box to add a new
1584 :type all_labels: dict
1585 :type selected_labels: dict
1586 :type show_new: bool
1587 :returns: A list of dbc rows with dropdown selection boxes.
1598 """Generates a dbc row with dropdown boxes.
1600 :param id: A string added to the dropdown ID.
1601 :param lopts: A list of options for 'label' dropdown.
1602 :param lval: Value of 'label' dropdown.
1603 :param vopts: A list of options for 'value' dropdown.
1604 :param vvals: A list of values for 'value' dropdown.
1610 :returns: dbc row with dropdown boxes.
1622 "index": f"label-{id}"
1624 placeholder="Select a label...",
1628 value=lval if lval else None
1641 "index": f"value-{id}"
1643 placeholder="Select a value...",
1647 value=vvals if vvals else None
1653 return dbc.Row(class_name="g-0 p-1", children=children)
1657 # Display rows with items in 'selected_labels'; label on the left,
1658 # values on the right:
1659 keys_left = list(all_labels.keys())
1660 for idx, label in enumerate(selected_labels.keys()):
1661 container.append(_row(
1663 lopts=deepcopy(keys_left),
1665 vopts=all_labels[label],
1666 vvals=selected_labels[label]
1668 keys_left.remove(label)
1670 # Display row with dd with labels on the left, right side is empty:
1671 if show_new and keys_left:
1672 container.append(_row(id="new", lopts=keys_left))
1677 Output("metadata-tput-lat", "children"),
1678 Output("metadata-hdrh-graph", "children"),
1679 Output("offcanvas-metadata", "is_open"),
1680 Input({"type": "graph", "index": ALL}, "clickData"),
1681 prevent_initial_call=True
1683 def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1684 """Generates the data for the offcanvas displayed when a particular
1685 point in a graph is clicked on.
1687 :param graph_data: The data from the clicked point in the graph.
1688 :type graph_data: dict
1689 :returns: The data to be displayed on the offcanvas and the
1690 information to show the offcanvas.
1691 :rtype: tuple(list, list, bool)
1694 trigger = Trigger(callback_context.triggered)
1697 if trigger.idx == "tput":
1699 elif trigger.idx == "bandwidth":
1701 elif trigger.idx == "lat":
1705 graph_data = graph_data[idx]["points"][0]
1706 except (IndexError, KeyError, ValueError, TypeError):
1709 metadata = no_update
1712 list_group_items = list()
1713 for itm in graph_data.get("text", None).split("<br>"):
1716 lst_itm = itm.split(": ")
1717 if lst_itm[0] == "csit-ref":
1718 list_group_item = dbc.ListGroupItem([
1719 dbc.Badge(lst_itm[0]),
1722 href=f"{C.URL_JENKINS}{lst_itm[1]}",
1727 list_group_item = dbc.ListGroupItem([
1728 dbc.Badge(lst_itm[0]),
1731 list_group_items.append(list_group_item)
1733 if trigger.idx == "tput":
1734 title = "Throughput"
1735 elif trigger.idx == "bandwidth":
1737 elif trigger.idx == "lat":
1739 hdrh_data = graph_data.get("customdata", None)
1742 class_name="gy-2 p-0",
1744 dbc.CardHeader(hdrh_data.pop("name")),
1747 id="hdrh-latency-graph",
1748 figure=graph_hdrh_latency(
1749 hdrh_data, self._graph_layout
1760 class_name="gy-2 p-0",
1762 dbc.CardHeader(children=[
1764 target_id="tput-lat-metadata",
1766 style={"display": "inline-block"}
1771 dbc.ListGroup(list_group_items, flush=True),
1772 id="tput-lat-metadata",
1779 return metadata, graph, True
1782 Output("download-trending-data", "data"),
1783 State("store", "data"),
1784 Input("plot-btn-download", "n_clicks"),
1785 Input({"type": "tm-btn-download", "index": ALL}, "n_clicks"),
1786 prevent_initial_call=True
1788 def _download_data(store: list, *_) -> dict:
1789 """Download the data
1791 :param store_sel: List of tests selected by user stored in the
1793 :type store_sel: list
1794 :returns: dict of data frame content (base64 encoded) and meta data
1795 used by the Download component.
1801 if not store["selected-tests"]:
1806 trigger = Trigger(callback_context.triggered)
1807 if not trigger.value:
1810 if trigger.type == "plot-btn-download":
1812 for itm in store["selected-tests"]:
1813 sel_data = select_trending_data(self._data, itm)
1814 if sel_data is None:
1816 data.append(sel_data)
1817 df = pd.concat(data, ignore_index=True, copy=False)
1818 file_name = C.TREND_DOWNLOAD_FILE_NAME
1819 elif trigger.type == "tm-btn-download":
1820 tm = TelemetryData(store["selected-tests"])
1821 tm.from_json(store["telemetry-data"])
1822 df = tm.select_tm_trending_data(
1823 store["telemetry-panels"][trigger.idx]
1825 file_name = C.TELEMETRY_DOWNLOAD_FILE_NAME
1829 return dcc.send_data_frame(df.to_csv, file_name)
1832 Output("offcanvas-documentation", "is_open"),
1833 Input("btn-documentation", "n_clicks"),
1834 State("offcanvas-documentation", "is_open")
1836 def toggle_offcanvas_documentation(n_clicks, is_open):