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
38 from ..utils.url_processing import url_decode
39 from ..data.data import Data
40 from .graphs import graph_trending, graph_hdrh_latency, select_trending_data, \
44 # Control panel partameters and their default values.
50 "dd-area-opt": list(),
53 "dd-test-opt": list(),
56 "cl-core-opt": list(),
57 "cl-core-val": list(),
58 "cl-core-all-val": list(),
59 "cl-core-all-opt": C.CL_ALL_DISABLED,
60 "cl-frmsize-opt": list(),
61 "cl-frmsize-val": list(),
62 "cl-frmsize-all-val": list(),
63 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
64 "cl-tsttype-opt": list(),
65 "cl-tsttype-val": list(),
66 "cl-tsttype-all-val": list(),
67 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
69 "cl-normalize-val": list()
74 """The layout of the dash app and the callbacks.
77 def __init__(self, app: Flask, html_layout_file: str,
78 graph_layout_file: str, data_spec_file: str, tooltip_file: str,
79 time_period: str=None) -> None:
81 - save the input parameters,
82 - read and pre-process the data,
83 - prepare data for the control panel,
84 - read HTML layout file,
85 - read tooltips from the tooltip file.
87 :param app: Flask application running the dash application.
88 :param html_layout_file: Path and name of the file specifying the HTML
89 layout of the dash application.
90 :param graph_layout_file: Path and name of the file with layout of
92 :param data_spec_file: Path and name of the file specifying the data to
93 be read from parquets for this application.
94 :param tooltip_file: Path and name of the yaml file specifying the
96 :param time_period: It defines the time period for data read from the
97 parquets in days from now back to the past.
99 :type html_layout_file: str
100 :type graph_layout_file: str
101 :type data_spec_file: str
102 :type tooltip_file: str
103 :type time_period: int
108 self._html_layout_file = html_layout_file
109 self._graph_layout_file = graph_layout_file
110 self._data_spec_file = data_spec_file
111 self._tooltip_file = tooltip_file
112 self._time_period = time_period
116 data_spec_file=self._data_spec_file,
118 ).read_trending_mrr(days=self._time_period)
121 data_spec_file=self._data_spec_file,
123 ).read_trending_ndrpdr(days=self._time_period)
125 self._data = pd.concat(
126 [data_mrr, data_ndrpdr],
131 # Get structure of tests:
133 for _, row in self._data[["job", "test_id"]].drop_duplicates().\
135 lst_job = row["job"].split("-")
138 tbed = "-".join(lst_job[-2:])
139 lst_test = row["test_id"].split(".")
143 area = "-".join(lst_test[3:-2])
144 suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\
147 nic = suite.split("-")[0]
148 for drv in C.DRIVERS:
154 test = test.replace(f"{drv}-", "")
158 infra = "-".join((tbed, nic, driver))
159 lst_test = test.split("-")
160 framesize = lst_test[0]
161 core = lst_test[1] if lst_test[1] else "8C"
162 test = "-".join(lst_test[2: -1])
164 if tbs.get(dut, None) is None:
166 if tbs[dut].get(infra, None) is None:
167 tbs[dut][infra] = dict()
168 if tbs[dut][infra].get(area, None) is None:
169 tbs[dut][infra][area] = dict()
170 if tbs[dut][infra][area].get(test, None) is None:
171 tbs[dut][infra][area][test] = dict()
172 tbs[dut][infra][area][test]["core"] = list()
173 tbs[dut][infra][area][test]["frame-size"] = list()
174 tbs[dut][infra][area][test]["test-type"] = list()
175 if core.upper() not in tbs[dut][infra][area][test]["core"]:
176 tbs[dut][infra][area][test]["core"].append(core.upper())
177 if framesize.upper() not in \
178 tbs[dut][infra][area][test]["frame-size"]:
179 tbs[dut][infra][area][test]["frame-size"].append(
183 if "MRR" not in tbs[dut][infra][area][test]["test-type"]:
184 tbs[dut][infra][area][test]["test-type"].append("MRR")
185 elif ttype == "ndrpdr":
186 if "NDR" not in tbs[dut][infra][area][test]["test-type"]:
187 tbs[dut][infra][area][test]["test-type"].extend(
193 self._html_layout = str()
194 self._graph_layout = None
195 self._tooltips = dict()
198 with open(self._html_layout_file, "r") as file_read:
199 self._html_layout = file_read.read()
200 except IOError as err:
202 f"Not possible to open the file {self._html_layout_file}\n{err}"
206 with open(self._graph_layout_file, "r") as file_read:
207 self._graph_layout = load(file_read, Loader=FullLoader)
208 except IOError as err:
210 f"Not possible to open the file {self._graph_layout_file}\n"
213 except YAMLError as err:
215 f"An error occurred while parsing the specification file "
216 f"{self._graph_layout_file}\n{err}"
220 with open(self._tooltip_file, "r") as file_read:
221 self._tooltips = load(file_read, Loader=FullLoader)
222 except IOError as err:
224 f"Not possible to open the file {self._tooltip_file}\n{err}"
226 except YAMLError as err:
228 f"An error occurred while parsing the specification file "
229 f"{self._tooltip_file}\n{err}"
233 if self._app is not None and hasattr(self, "callbacks"):
234 self.callbacks(self._app)
237 def html_layout(self):
238 return self._html_layout
240 def add_content(self):
241 """Top level method which generated the web page.
244 - Store for user input data,
246 - Main area with control panel and ploting area.
248 If no HTML layout is provided, an error message is displayed instead.
250 :returns: The HTML div with the whole page.
254 if self.html_layout and self._spec_tbs:
259 dcc.Store(id="store-selected-tests"),
260 dcc.Store(id="store-control-panel"),
261 dcc.Store(id="store-telemetry-data"),
262 dcc.Store(id="store-telemetry-user"),
263 dcc.Location(id="url", refresh=False),
275 self._add_ctrl_col(),
276 self._add_plotting_col()
282 id="offcanvas-metadata",
283 title="Throughput And Latency",
287 dbc.Row(id="metadata-tput-lat"),
288 dbc.Row(id="metadata-hdrh-graph")
291 delay_show=C.SPINNER_DELAY
308 def _add_navbar(self):
309 """Add nav element with navigation panel. It is placed on the top.
311 :returns: Navigation bar.
312 :rtype: dbc.NavbarSimple
314 return dbc.NavbarSimple(
315 id="navbarsimple-main",
328 brand_external_link=True,
333 def _add_ctrl_col(self) -> dbc.Col:
334 """Add column with controls. It is placed on the left side.
336 :returns: Column with the control panel.
341 children=self._add_ctrl_panel(),
342 className="sticky-top"
346 def _add_ctrl_panel(self) -> list:
347 """Add control panel.
349 :returns: Control panel.
354 class_name="g-0 p-1",
359 children=show_tooltip(
366 id={"type": "ctrl-dd", "index": "dut"},
367 placeholder="Select a Device under Test...",
370 {"label": k, "value": k} \
371 for k in self._spec_tbs.keys()
373 key=lambda d: d["label"]
382 class_name="g-0 p-1",
387 children=show_tooltip(
394 id={"type": "ctrl-dd", "index": "phy"},
396 "Select a Physical Test Bed Topology..."
404 class_name="g-0 p-1",
409 children=show_tooltip(
416 id={"type": "ctrl-dd", "index": "area"},
417 placeholder="Select an Area..."
425 class_name="g-0 p-1",
430 children=show_tooltip(
437 id={"type": "ctrl-dd", "index": "test"},
438 placeholder="Select a Test..."
446 class_name="g-0 p-1",
451 children=show_tooltip(
462 "index": "frmsize-all"
464 options=C.CL_ALL_DISABLED,
483 style={"align-items": "center"},
489 class_name="g-0 p-1",
494 children=show_tooltip(
507 options=C.CL_ALL_DISABLED,
526 style={"align-items": "center"},
532 class_name="g-0 p-1",
537 children=show_tooltip(
548 "index": "tsttype-all"
550 options=C.CL_ALL_DISABLED,
569 style={"align-items": "center"},
575 class_name="g-0 p-1",
580 children=show_tooltip(
591 "value": "normalize",
593 "Normalize to CPU frequency "
604 style={"align-items": "center"},
610 class_name="g-0 p-1",
613 id={"type": "ctrl-btn", "index": "add-test"},
614 children="Add Selected",
620 id="row-card-sel-tests",
621 class_name="g-0 p-1",
622 style=C.STYLE_DISABLED,
625 class_name="overflow-auto p-0",
628 style={"max-height": "14em"},
634 id="row-btns-sel-tests",
635 class_name="g-0 p-1",
636 style=C.STYLE_DISABLED,
641 id={"type": "ctrl-btn", "index": "rm-test"},
642 children="Remove Selected",
648 id={"type": "ctrl-btn", "index": "rm-test-all"},
649 children="Remove All",
660 def _add_plotting_col(self) -> dbc.Col:
661 """Add column with plots. It is placed on the right side.
663 :returns: Column with plots.
667 id="col-plotting-area",
671 id="plotting-area-trending",
672 class_name="g-0 p-0",
673 children=C.PLACEHOLDER
675 delay_show=C.SPINNER_DELAY
678 id="plotting-area-telemetry",
679 class_name="g-0 p-0",
680 children=C.PLACEHOLDER
683 id="plotting-area-buttons",
684 class_name="g-0 p-0",
685 children=C.PLACEHOLDER
691 def _get_plotting_area_buttons(self) -> dbc.Col:
692 """Add buttons and modals to the plotting area.
694 :returns: A column with buttons and modals for telemetry.
701 id={"type": "telemetry-btn", "index": "open"},
702 children="Add Panel with Telemetry",
706 "text-transform": "none",
707 "padding": "0rem 1rem"
720 id="plot-mod-telemetry-body-1",
721 children=self._get_telemetry_step_1()
723 delay_show=2*C.SPINNER_DELAY
729 "type": "telemetry-btn",
737 "type": "telemetry-btn",
744 id="plot-mod-telemetry-1",
761 id="plot-mod-telemetry-body-2",
762 children=self._get_telemetry_step_2()
764 delay_show=2*C.SPINNER_DELAY
770 "type": "telemetry-btn",
778 "type": "telemetry-btn",
786 "type": "telemetry-btn",
793 id="plot-mod-telemetry-2",
801 className="d-grid gap-0 d-md-flex justify-content-md-end"
805 def _get_plotting_area_trending(
811 """Generate the plotting area with all its content.
813 :param tests: A list of tests to be displayed in the trending graphs.
814 :param normalize: If True, the data in graphs is normalized.
815 :param url: An URL to be displayed in the modal window.
817 :type normalize: bool
819 :returns: A collumn with trending graphs (tput and latency) in tabs.
825 figs = graph_trending(self._data, tests, self._graph_layout, normalize)
833 id={"type": "graph", "index": "tput"},
845 id={"type": "graph", "index": "lat"},
858 active_tab="tab-tput",
871 "text-transform": "none",
872 "padding": "0rem 1rem"
877 dbc.ModalHeader(dbc.ModalTitle("URL")),
886 id="plot-btn-download",
887 children="Download Data",
891 "text-transform": "none",
892 "padding": "0rem 1rem"
895 dcc.Download(id="download-trending-data")
898 "d-grid gap-0 d-md-flex justify-content-md-end"
915 class_name="g-0 p-1",
916 start_collapsed=False,
918 active_item=["item-0", ]
920 class_name="g-0 p-0",
925 def _get_plotting_area_telemetry(self, graphs: list) -> dbc.Col:
926 """Generate the plotting area with telemetry.
935 title=f"Telemetry: {graph[1]}",
937 id={"type": "graph-telemetry", "index": graph[1]},
948 class_name="g-0 p-1",
949 start_collapsed=False,
951 active_item=[f"item-{i}" for i in range(len(acc_items))]
953 class_name="g-0 p-0",
959 def _get_telemetry_step_1() -> list:
960 """Return the content of the modal window used in the step 1 of metrics
963 :returns: A list of dbc rows with 'input' and 'search output'.
968 class_name="g-0 p-1",
971 id="telemetry-search-in",
972 placeholder="Start typing a metric name...",
978 class_name="g-0 p-1",
981 class_name="overflow-auto p-0",
982 id="telemetry-search-out",
984 style={"max-height": "14em"},
992 def _get_telemetry_step_2() -> list:
993 """Return the content of the modal window used in the step 2 of metrics
996 :returns: A list of dbc rows with 'container with dynamic dropdowns' and
1003 class_name="g-0 p-1",
1004 children=["Add content here."]
1007 class_name="g-0 p-1",
1010 id="telemetry-list-metrics",
1020 def callbacks(self, app):
1021 """Callbacks for the whole application.
1023 :param app: The application.
1029 Output("store-control-panel", "data"),
1030 Output("store-selected-tests", "data"),
1031 Output("plotting-area-trending", "children"),
1032 Output("plotting-area-buttons", "children"),
1033 Output("row-card-sel-tests", "style"),
1034 Output("row-btns-sel-tests", "style"),
1035 Output("lg-selected", "children"),
1036 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
1037 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
1038 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
1039 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
1040 Output({"type": "ctrl-dd", "index": "area"}, "options"),
1041 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
1042 Output({"type": "ctrl-dd", "index": "area"}, "value"),
1043 Output({"type": "ctrl-dd", "index": "test"}, "options"),
1044 Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
1045 Output({"type": "ctrl-dd", "index": "test"}, "value"),
1046 Output({"type": "ctrl-cl", "index": "core"}, "options"),
1047 Output({"type": "ctrl-cl", "index": "core"}, "value"),
1048 Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
1049 Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
1050 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
1051 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
1052 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
1053 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
1054 Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
1055 Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
1056 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
1057 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
1058 Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
1059 Output("normalize", "value")
1062 State("store-control-panel", "data"),
1063 State("store-selected-tests", "data"),
1064 State({"type": "sel-cl", "index": ALL}, "value")
1067 Input("url", "href"),
1068 Input("normalize", "value"),
1069 Input({"type": "ctrl-dd", "index": ALL}, "value"),
1070 Input({"type": "ctrl-cl", "index": ALL}, "value"),
1071 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
1073 prevent_initial_call=True
1075 def _update_application(
1076 control_panel: dict,
1083 """Update the application when the event is detected.
1086 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
1090 parsed_url = url_decode(href)
1092 url_params = parsed_url["params"]
1096 trigger = Trigger(callback_context.triggered)
1098 if trigger.type == "url" and url_params:
1100 store_sel = literal_eval(url_params["store_sel"][0])
1101 normalize = literal_eval(url_params["norm"][0])
1102 except (KeyError, IndexError):
1105 last_test = store_sel[-1]
1106 test = self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1107 [last_test["area"]][last_test["test"]]
1109 "dd-dut-val": last_test["dut"],
1110 "dd-phy-val": last_test["phy"],
1111 "dd-phy-opt": generate_options(
1112 self._spec_tbs[last_test["dut"]].keys()
1114 "dd-phy-dis": False,
1115 "dd-area-val": last_test["area"],
1117 {"label": label(v), "value": v} for v in sorted(
1118 self._spec_tbs[last_test["dut"]]\
1119 [last_test["phy"]].keys()
1122 "dd-area-dis": False,
1123 "dd-test-val": last_test["test"],
1124 "dd-test-opt": generate_options(
1125 self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1126 [last_test["area"]].keys()
1128 "dd-test-dis": False,
1129 "cl-core-opt": generate_options(test["core"]),
1130 "cl-core-val": [last_test["core"].upper(), ],
1131 "cl-core-all-val": list(),
1132 "cl-core-all-opt": C.CL_ALL_ENABLED,
1133 "cl-frmsize-opt": generate_options(test["frame-size"]),
1134 "cl-frmsize-val": [last_test["framesize"].upper(), ],
1135 "cl-frmsize-all-val": list(),
1136 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1137 "cl-tsttype-opt": generate_options(test["test-type"]),
1138 "cl-tsttype-val": [last_test["testtype"].upper(), ],
1139 "cl-tsttype-all-val": list(),
1140 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1141 "cl-normalize-val": normalize,
1142 "btn-add-dis": False
1145 elif trigger.type == "normalize":
1146 ctrl_panel.set({"cl-normalize-val": normalize})
1148 elif trigger.type == "ctrl-dd":
1149 if trigger.idx == "dut":
1151 options = generate_options(
1152 self._spec_tbs[trigger.value].keys()
1159 "dd-dut-val": trigger.value,
1160 "dd-phy-val": str(),
1161 "dd-phy-opt": options,
1162 "dd-phy-dis": disabled,
1163 "dd-area-val": str(),
1164 "dd-area-opt": list(),
1165 "dd-area-dis": True,
1166 "dd-test-val": str(),
1167 "dd-test-opt": list(),
1168 "dd-test-dis": True,
1169 "cl-core-opt": list(),
1170 "cl-core-val": list(),
1171 "cl-core-all-val": list(),
1172 "cl-core-all-opt": C.CL_ALL_DISABLED,
1173 "cl-frmsize-opt": list(),
1174 "cl-frmsize-val": list(),
1175 "cl-frmsize-all-val": list(),
1176 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1177 "cl-tsttype-opt": list(),
1178 "cl-tsttype-val": list(),
1179 "cl-tsttype-all-val": list(),
1180 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1183 elif trigger.idx == "phy":
1185 dut = ctrl_panel.get("dd-dut-val")
1186 phy = self._spec_tbs[dut][trigger.value]
1187 options = [{"label": label(v), "value": v} \
1188 for v in sorted(phy.keys())]
1194 "dd-phy-val": trigger.value,
1195 "dd-area-val": str(),
1196 "dd-area-opt": options,
1197 "dd-area-dis": disabled,
1198 "dd-test-val": str(),
1199 "dd-test-opt": list(),
1200 "dd-test-dis": True,
1201 "cl-core-opt": list(),
1202 "cl-core-val": list(),
1203 "cl-core-all-val": list(),
1204 "cl-core-all-opt": C.CL_ALL_DISABLED,
1205 "cl-frmsize-opt": list(),
1206 "cl-frmsize-val": list(),
1207 "cl-frmsize-all-val": list(),
1208 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1209 "cl-tsttype-opt": list(),
1210 "cl-tsttype-val": list(),
1211 "cl-tsttype-all-val": list(),
1212 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1215 elif trigger.idx == "area":
1217 dut = ctrl_panel.get("dd-dut-val")
1218 phy = ctrl_panel.get("dd-phy-val")
1219 area = self._spec_tbs[dut][phy][trigger.value]
1220 options = generate_options(area.keys())
1226 "dd-area-val": trigger.value,
1227 "dd-test-val": str(),
1228 "dd-test-opt": options,
1229 "dd-test-dis": disabled,
1230 "cl-core-opt": list(),
1231 "cl-core-val": list(),
1232 "cl-core-all-val": list(),
1233 "cl-core-all-opt": C.CL_ALL_DISABLED,
1234 "cl-frmsize-opt": list(),
1235 "cl-frmsize-val": list(),
1236 "cl-frmsize-all-val": list(),
1237 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1238 "cl-tsttype-opt": list(),
1239 "cl-tsttype-val": list(),
1240 "cl-tsttype-all-val": list(),
1241 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1244 elif trigger.idx == "test":
1245 dut = ctrl_panel.get("dd-dut-val")
1246 phy = ctrl_panel.get("dd-phy-val")
1247 area = ctrl_panel.get("dd-area-val")
1248 if all((dut, phy, area, trigger.value, )):
1249 test = self._spec_tbs[dut][phy][area][trigger.value]
1251 "dd-test-val": trigger.value,
1252 "cl-core-opt": generate_options(test["core"]),
1253 "cl-core-val": list(),
1254 "cl-core-all-val": list(),
1255 "cl-core-all-opt": C.CL_ALL_ENABLED,
1257 generate_options(test["frame-size"]),
1258 "cl-frmsize-val": list(),
1259 "cl-frmsize-all-val": list(),
1260 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1262 generate_options(test["test-type"]),
1263 "cl-tsttype-val": list(),
1264 "cl-tsttype-all-val": list(),
1265 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1268 elif trigger.type == "ctrl-cl":
1269 param = trigger.idx.split("-")[0]
1270 if "-all" in trigger.idx:
1271 c_sel, c_all, c_id = list(), trigger.value, "all"
1273 c_sel, c_all, c_id = trigger.value, list(), str()
1274 val_sel, val_all = sync_checklists(
1275 options=ctrl_panel.get(f"cl-{param}-opt"),
1281 f"cl-{param}-val": val_sel,
1282 f"cl-{param}-all-val": val_all,
1284 if all((ctrl_panel.get("cl-core-val"),
1285 ctrl_panel.get("cl-frmsize-val"),
1286 ctrl_panel.get("cl-tsttype-val"), )):
1287 ctrl_panel.set({"btn-add-dis": False})
1289 ctrl_panel.set({"btn-add-dis": True})
1290 elif trigger.type == "ctrl-btn":
1292 if trigger.idx == "add-test":
1293 dut = ctrl_panel.get("dd-dut-val")
1294 phy = ctrl_panel.get("dd-phy-val")
1295 area = ctrl_panel.get("dd-area-val")
1296 test = ctrl_panel.get("dd-test-val")
1297 # Add selected test(s) to the list of tests in store:
1298 if store_sel is None:
1300 for core in ctrl_panel.get("cl-core-val"):
1301 for framesize in ctrl_panel.get("cl-frmsize-val"):
1302 for ttype in ctrl_panel.get("cl-tsttype-val"):
1307 phy.replace('af_xdp', 'af-xdp'),
1314 if tid not in [i["id"] for i in store_sel]:
1321 "framesize": framesize.lower(),
1322 "core": core.lower(),
1323 "testtype": ttype.lower()
1325 store_sel = sorted(store_sel, key=lambda d: d["id"])
1326 if C.CLEAR_ALL_INPUTS:
1327 ctrl_panel.set(ctrl_panel.defaults)
1328 elif trigger.idx == "rm-test" and lst_sel:
1329 new_store_sel = list()
1330 for idx, item in enumerate(store_sel):
1331 if not lst_sel[idx]:
1332 new_store_sel.append(item)
1333 store_sel = new_store_sel
1334 elif trigger.idx == "rm-test-all":
1339 lg_selected = get_list_group_items(store_sel, "sel-cl")
1340 plotting_area_trending = self._get_plotting_area_trending(
1345 {"store_sel": store_sel, "norm": normalize}
1348 plotting_area_buttons = self._get_plotting_area_buttons()
1349 row_card_sel_tests = C.STYLE_ENABLED
1350 row_btns_sel_tests = C.STYLE_ENABLED
1352 plotting_area_trending = C.PLACEHOLDER
1353 plotting_area_buttons = C.PLACEHOLDER
1354 row_card_sel_tests = C.STYLE_DISABLED
1355 row_btns_sel_tests = C.STYLE_DISABLED
1356 lg_selected = no_update
1359 plotting_area_trending = no_update
1360 plotting_area_buttons = no_update
1361 row_card_sel_tests = no_update
1362 row_btns_sel_tests = no_update
1363 lg_selected = no_update
1368 plotting_area_trending,
1369 plotting_area_buttons,
1374 ret_val.extend(ctrl_panel.values)
1378 Output("plot-mod-url", "is_open"),
1379 Input("plot-btn-url", "n_clicks"),
1380 State("plot-mod-url", "is_open")
1382 def toggle_plot_mod_url(n, is_open):
1383 """Toggle the modal window with url.
1390 Output("store-telemetry-data", "data"),
1391 Output("store-telemetry-user", "data"),
1392 Output("telemetry-search-in", "value"),
1393 Output("telemetry-search-out", "children"),
1394 Output("telemetry-list-metrics", "value"),
1395 Output("telemetry-dd", "children"),
1396 Output("plotting-area-telemetry", "children"),
1397 Output("plot-mod-telemetry-1", "is_open"),
1398 Output("plot-mod-telemetry-2", "is_open"),
1399 Output({"type": "telemetry-btn", "index": "select"}, "disabled"),
1400 Output({"type": "telemetry-btn", "index": "add"}, "disabled"),
1401 State("store-telemetry-data", "data"),
1402 State("store-telemetry-user", "data"),
1403 State("store-selected-tests", "data"),
1404 Input({"type": "tele-cl", "index": ALL}, "value"),
1405 Input("telemetry-search-in", "value"),
1406 Input({"type": "telemetry-btn", "index": ALL}, "n_clicks"),
1407 Input({"type": "tm-dd", "index": ALL}, "value"),
1408 prevent_initial_call=True
1410 def _update_plot_mod_telemetry(
1419 """Toggle the modal window with telemetry.
1422 if not any(n_clicks):
1426 # Telemetry user data
1427 # The data provided by user or result of user action
1429 # List of unique metrics:
1430 "unique_metrics": list(),
1431 # List of metrics selected by user:
1432 "selected_metrics": list(),
1433 # Labels from metrics selected by user (key: label name,
1434 # value: list of all possible values):
1435 "unique_labels": dict(),
1436 # Labels selected by the user (subset of 'unique_labels'):
1437 "selected_labels": dict(),
1438 # All unique metrics with labels (output from the step 1)
1439 # converted from pandas dataframe to dictionary.
1440 "unique_metrics_with_labels": dict(),
1441 # Metrics with labels selected by the user using dropdowns.
1442 "selected_metrics_with_labels": dict()
1445 tm = TelemetryData(tests=store_sel)
1447 search_out = no_update
1448 list_metrics = no_update
1450 plotting_area_telemetry = no_update
1451 is_open = (False, False)
1452 is_btn_disabled = (True, True)
1454 trigger = Trigger(callback_context.triggered)
1455 if trigger.type == "telemetry-btn":
1456 if trigger.idx in ("open", "back"):
1457 tm.from_dataframe(self._data)
1458 tm_json = tm.to_json()
1459 tm_user["unique_metrics"] = tm.unique_metrics
1460 tm_user["selected_metrics"] = list()
1461 tm_user["unique_labels"] = dict()
1462 tm_user["selected_labels"] = dict()
1464 search_out = get_list_group_items(
1465 tm_user["unique_metrics"],
1469 is_open = (True, False)
1470 elif trigger.idx == "select":
1471 tm.from_json(tm_data)
1473 if not tm_user["selected_metrics"]:
1474 tm_user["selected_metrics"] = \
1475 tm_user["unique_metrics"]
1476 metrics = [a for a, b in \
1477 zip(tm_user["selected_metrics"], cl_metrics) if b]
1478 tm_user["selected_metrics"] = metrics
1479 tm_user["unique_labels"] = \
1480 tm.get_selected_labels(metrics)
1481 tm_user["unique_metrics_with_labels"] = \
1482 tm.unique_metrics_with_labels
1483 list_metrics = tm.str_metrics
1484 tm_dd = _get_dd_container(tm_user["unique_labels"])
1486 is_btn_disabled = (True, False)
1487 is_open = (False, True)
1490 is_open = (False, False)
1491 elif trigger.idx == "add":
1492 tm.from_json(tm_data)
1493 plotting_area_telemetry = self._get_plotting_area_telemetry(
1495 tm.select_tm_trending_data(
1496 tm_user["selected_metrics_with_labels"]
1501 is_open = (False, False)
1502 elif trigger.idx == "cancel":
1504 is_open = (False, False)
1505 elif trigger.type == "telemetry-search-in":
1506 tm.from_metrics(tm_user["unique_metrics"])
1507 tm_user["selected_metrics"] = \
1508 tm.search_unique_metrics(search_in)
1509 search_out = get_list_group_items(
1510 tm_user["selected_metrics"],
1514 is_open = (True, False)
1515 elif trigger.type == "tele-cl":
1517 is_btn_disabled = (False, True)
1518 is_open = (True, False)
1519 elif trigger.type == "tm-dd":
1520 tm.from_metrics_with_labels(
1521 tm_user["unique_metrics_with_labels"]
1525 for itm in tm_dd_in:
1528 elif isinstance(itm, str):
1530 selected[itm] = list()
1531 elif isinstance(itm, list):
1532 if previous_itm is not None:
1533 selected[previous_itm] = itm
1537 tm_dd = _get_dd_container(
1538 tm_user["unique_labels"],
1542 sel_metrics = tm.filter_selected_metrics_by_labels(selected)
1543 tm_user["selected_metrics_with_labels"] = sel_metrics.to_dict()
1544 if not sel_metrics.empty:
1545 list_metrics = tm.metrics_to_str(sel_metrics)
1547 list_metrics = str()
1549 is_btn_disabled = (True, False)
1550 is_open = (False, True)
1560 plotting_area_telemetry
1562 ret_val.extend(is_open)
1563 ret_val.extend(is_btn_disabled)
1566 def _get_dd_container(
1568 selected_labels: dict=dict(),
1571 """Generate a container with dropdown selection boxes depenting on
1574 :param all_labels: A dictionary with unique labels and their
1576 :param selected_labels: A dictionalry with user selected lables and
1578 :param show_new: If True, a dropdown selection box to add a new
1580 :type all_labels: dict
1581 :type selected_labels: dict
1582 :type show_new: bool
1583 :returns: A list of dbc rows with dropdown selection boxes.
1594 """Generates a dbc row with dropdown boxes.
1596 :param id: A string added to the dropdown ID.
1597 :param lopts: A list of options for 'label' dropdown.
1598 :param lval: Value of 'label' dropdown.
1599 :param vopts: A list of options for 'value' dropdown.
1600 :param vvals: A list of values for 'value' dropdown.
1606 :returns: dbc row with dropdown boxes.
1618 "index": f"label-{id}"
1620 placeholder="Select a label...",
1624 value=lval if lval else None
1637 "index": f"value-{id}"
1639 placeholder="Select a value...",
1643 value=vvals if vvals else None
1649 return dbc.Row(class_name="g-0 p-1", children=children)
1653 # Display rows with items in 'selected_labels'; label on the left,
1654 # values on the right:
1655 keys_left = list(all_labels.keys())
1656 for idx, label in enumerate(selected_labels.keys()):
1657 container.append(_row(
1659 lopts=deepcopy(keys_left),
1661 vopts=all_labels[label],
1662 vvals=selected_labels[label]
1664 keys_left.remove(label)
1666 # Display row with dd with labels on the left, right side is empty:
1667 if show_new and keys_left:
1668 container.append(_row(id="new", lopts=keys_left))
1673 Output("metadata-tput-lat", "children"),
1674 Output("metadata-hdrh-graph", "children"),
1675 Output("offcanvas-metadata", "is_open"),
1676 Input({"type": "graph", "index": ALL}, "clickData"),
1677 prevent_initial_call=True
1679 def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1680 """Generates the data for the offcanvas displayed when a particular
1681 point in a graph is clicked on.
1683 :param graph_data: The data from the clicked point in the graph.
1684 :type graph_data: dict
1685 :returns: The data to be displayed on the offcanvas and the
1686 information to show the offcanvas.
1687 :rtype: tuple(list, list, bool)
1690 trigger = Trigger(callback_context.triggered)
1693 idx = 0 if trigger.idx == "tput" else 1
1694 graph_data = graph_data[idx]["points"][0]
1695 except (IndexError, KeyError, ValueError, TypeError):
1698 metadata = no_update
1703 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1704 ) for x in graph_data.get("text", "").split("<br>")
1706 if trigger.idx == "tput":
1707 title = "Throughput"
1708 elif trigger.idx == "lat":
1710 hdrh_data = graph_data.get("customdata", None)
1713 class_name="gy-2 p-0",
1715 dbc.CardHeader(hdrh_data.pop("name")),
1716 dbc.CardBody(children=[
1718 id="hdrh-latency-graph",
1719 figure=graph_hdrh_latency(
1720 hdrh_data, self._graph_layout
1731 class_name="gy-2 p-0",
1733 dbc.CardHeader(children=[
1735 target_id="tput-lat-metadata",
1737 style={"display": "inline-block"}
1742 id="tput-lat-metadata",
1744 children=[dbc.ListGroup(children, flush=True), ]
1750 return metadata, graph, True
1753 Output("download-trending-data", "data"),
1754 State("store-selected-tests", "data"),
1755 Input("plot-btn-download", "n_clicks"),
1756 prevent_initial_call=True
1758 def _download_trending_data(store_sel: list, _) -> dict:
1759 """Download the data
1761 :param store_sel: List of tests selected by user stored in the
1763 :type store_sel: list
1764 :returns: dict of data frame content (base64 encoded) and meta data
1765 used by the Download component.
1773 for itm in store_sel:
1774 sel_data = select_trending_data(self._data, itm)
1775 if sel_data is None:
1777 df = pd.concat([df, sel_data], ignore_index=True, copy=False)
1779 return dcc.send_data_frame(df.to_csv, C.TREND_DOWNLOAD_FILE_NAME)