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 .graphs import graph_trending, graph_hdrh_latency, select_trending_data, \
43 # Control panel partameters and their default values.
49 "dd-area-opt": list(),
52 "dd-test-opt": list(),
55 "cl-core-opt": list(),
56 "cl-core-val": list(),
57 "cl-core-all-val": list(),
58 "cl-core-all-opt": C.CL_ALL_DISABLED,
59 "cl-frmsize-opt": list(),
60 "cl-frmsize-val": list(),
61 "cl-frmsize-all-val": list(),
62 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
63 "cl-tsttype-opt": list(),
64 "cl-tsttype-val": list(),
65 "cl-tsttype-all-val": list(),
66 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
68 "cl-normalize-val": list()
73 """The layout of the dash app and the callbacks.
78 data_trending: pd.DataFrame,
79 html_layout_file: str,
80 graph_layout_file: str,
84 - save the input parameters,
85 - read and pre-process the data,
86 - prepare data for the control panel,
87 - read HTML layout file,
88 - read tooltips from the tooltip file.
90 :param app: Flask application running the dash application.
91 :param data_trending: Pandas dataframe with trending data.
92 :param html_layout_file: Path and name of the file specifying the HTML
93 layout of the dash application.
94 :param graph_layout_file: Path and name of the file with layout of
96 :param tooltip_file: Path and name of the yaml file specifying the
99 :type data_trending: pandas.DataFrame
100 :type html_layout_file: str
101 :type graph_layout_file: str
102 :type tooltip_file: str
107 self._data = data_trending
108 self._html_layout_file = html_layout_file
109 self._graph_layout_file = graph_layout_file
110 self._tooltip_file = tooltip_file
112 # Get structure of tests:
114 for _, row in self._data[["job", "test_id"]].drop_duplicates().\
116 lst_job = row["job"].split("-")
119 tbed = "-".join(lst_job[-2:])
120 lst_test = row["test_id"].split(".")
124 area = "-".join(lst_test[3:-2])
125 suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\
128 nic = suite.split("-")[0]
129 for drv in C.DRIVERS:
135 test = test.replace(f"{drv}-", "")
139 infra = "-".join((tbed, nic, driver))
140 lst_test = test.split("-")
141 framesize = lst_test[0]
142 core = lst_test[1] if lst_test[1] else "8C"
143 test = "-".join(lst_test[2: -1])
145 if tbs.get(dut, None) is None:
147 if tbs[dut].get(infra, None) is None:
148 tbs[dut][infra] = dict()
149 if tbs[dut][infra].get(area, None) is None:
150 tbs[dut][infra][area] = dict()
151 if tbs[dut][infra][area].get(test, None) is None:
152 tbs[dut][infra][area][test] = dict()
153 tbs[dut][infra][area][test]["core"] = list()
154 tbs[dut][infra][area][test]["frame-size"] = list()
155 tbs[dut][infra][area][test]["test-type"] = list()
156 if core.upper() not in tbs[dut][infra][area][test]["core"]:
157 tbs[dut][infra][area][test]["core"].append(core.upper())
158 if framesize.upper() not in \
159 tbs[dut][infra][area][test]["frame-size"]:
160 tbs[dut][infra][area][test]["frame-size"].append(
164 if "MRR" not in tbs[dut][infra][area][test]["test-type"]:
165 tbs[dut][infra][area][test]["test-type"].append("MRR")
166 elif ttype == "ndrpdr":
167 if "NDR" not in tbs[dut][infra][area][test]["test-type"]:
168 tbs[dut][infra][area][test]["test-type"].extend(
174 self._html_layout = str()
175 self._graph_layout = None
176 self._tooltips = dict()
179 with open(self._html_layout_file, "r") as file_read:
180 self._html_layout = file_read.read()
181 except IOError as err:
183 f"Not possible to open the file {self._html_layout_file}\n{err}"
187 with open(self._graph_layout_file, "r") as file_read:
188 self._graph_layout = load(file_read, Loader=FullLoader)
189 except IOError as err:
191 f"Not possible to open the file {self._graph_layout_file}\n"
194 except YAMLError as err:
196 f"An error occurred while parsing the specification file "
197 f"{self._graph_layout_file}\n{err}"
201 with open(self._tooltip_file, "r") as file_read:
202 self._tooltips = load(file_read, Loader=FullLoader)
203 except IOError as err:
205 f"Not possible to open the file {self._tooltip_file}\n{err}"
207 except YAMLError as err:
209 f"An error occurred while parsing the specification file "
210 f"{self._tooltip_file}\n{err}"
214 if self._app is not None and hasattr(self, "callbacks"):
215 self.callbacks(self._app)
218 def html_layout(self):
219 return self._html_layout
221 def add_content(self):
222 """Top level method which generated the web page.
225 - Store for user input data,
227 - Main area with control panel and ploting area.
229 If no HTML layout is provided, an error message is displayed instead.
231 :returns: The HTML div with the whole page.
235 if self.html_layout and self._spec_tbs:
240 dcc.Store(id="store-selected-tests"),
241 dcc.Store(id="store-control-panel"),
242 dcc.Store(id="store-telemetry-data"),
243 dcc.Store(id="store-telemetry-user"),
244 dcc.Location(id="url", refresh=False),
256 self._add_ctrl_col(),
257 self._add_plotting_col()
263 id="offcanvas-metadata",
264 title="Throughput And Latency",
268 dbc.Row(id="metadata-tput-lat"),
269 dbc.Row(id="metadata-hdrh-graph")
272 delay_show=C.SPINNER_DELAY
289 def _add_navbar(self):
290 """Add nav element with navigation panel. It is placed on the top.
292 :returns: Navigation bar.
293 :rtype: dbc.NavbarSimple
295 return dbc.NavbarSimple(
296 id="navbarsimple-main",
309 brand_external_link=True,
314 def _add_ctrl_col(self) -> dbc.Col:
315 """Add column with controls. It is placed on the left side.
317 :returns: Column with the control panel.
322 children=self._add_ctrl_panel(),
323 className="sticky-top"
327 def _add_ctrl_panel(self) -> list:
328 """Add control panel.
330 :returns: Control panel.
335 class_name="g-0 p-1",
340 children=show_tooltip(
347 id={"type": "ctrl-dd", "index": "dut"},
348 placeholder="Select a Device under Test...",
351 {"label": k, "value": k} \
352 for k in self._spec_tbs.keys()
354 key=lambda d: d["label"]
363 class_name="g-0 p-1",
368 children=show_tooltip(
375 id={"type": "ctrl-dd", "index": "phy"},
377 "Select a Physical Test Bed Topology..."
385 class_name="g-0 p-1",
390 children=show_tooltip(
397 id={"type": "ctrl-dd", "index": "area"},
398 placeholder="Select an Area..."
406 class_name="g-0 p-1",
411 children=show_tooltip(
418 id={"type": "ctrl-dd", "index": "test"},
419 placeholder="Select a Test..."
427 class_name="g-0 p-1",
432 children=show_tooltip(
443 "index": "frmsize-all"
445 options=C.CL_ALL_DISABLED,
464 style={"align-items": "center"},
470 class_name="g-0 p-1",
475 children=show_tooltip(
488 options=C.CL_ALL_DISABLED,
507 style={"align-items": "center"},
513 class_name="g-0 p-1",
518 children=show_tooltip(
529 "index": "tsttype-all"
531 options=C.CL_ALL_DISABLED,
550 style={"align-items": "center"},
556 class_name="g-0 p-1",
561 children=show_tooltip(
572 "value": "normalize",
574 "Normalize to CPU frequency "
585 style={"align-items": "center"},
591 class_name="g-0 p-1",
594 id={"type": "ctrl-btn", "index": "add-test"},
595 children="Add Selected",
601 id="row-card-sel-tests",
602 class_name="g-0 p-1",
603 style=C.STYLE_DISABLED,
606 class_name="overflow-auto p-0",
609 style={"max-height": "14em"},
615 id="row-btns-sel-tests",
616 class_name="g-0 p-1",
617 style=C.STYLE_DISABLED,
622 id={"type": "ctrl-btn", "index": "rm-test"},
623 children="Remove Selected",
629 id={"type": "ctrl-btn", "index": "rm-test-all"},
630 children="Remove All",
641 def _add_plotting_col(self) -> dbc.Col:
642 """Add column with plots. It is placed on the right side.
644 :returns: Column with plots.
648 id="col-plotting-area",
652 id="plotting-area-trending",
653 class_name="g-0 p-0",
654 children=C.PLACEHOLDER
656 delay_show=C.SPINNER_DELAY
659 id="plotting-area-telemetry",
660 class_name="g-0 p-0",
661 children=C.PLACEHOLDER
664 id="plotting-area-buttons",
665 class_name="g-0 p-0",
666 children=C.PLACEHOLDER
672 def _get_plotting_area_buttons(self) -> dbc.Col:
673 """Add buttons and modals to the plotting area.
675 :returns: A column with buttons and modals for telemetry.
682 id={"type": "telemetry-btn", "index": "open"},
683 children="Add Panel with Telemetry",
687 "text-transform": "none",
688 "padding": "0rem 1rem"
701 id="plot-mod-telemetry-body-1",
702 children=self._get_telemetry_step_1()
704 delay_show=2*C.SPINNER_DELAY
710 "type": "telemetry-btn",
718 "type": "telemetry-btn",
725 id="plot-mod-telemetry-1",
742 id="plot-mod-telemetry-body-2",
743 children=self._get_telemetry_step_2()
745 delay_show=2*C.SPINNER_DELAY
751 "type": "telemetry-btn",
759 "type": "telemetry-btn",
767 "type": "telemetry-btn",
774 id="plot-mod-telemetry-2",
782 className="d-grid gap-0 d-md-flex justify-content-md-end"
786 def _get_plotting_area_trending(
792 """Generate the plotting area with all its content.
794 :param tests: A list of tests to be displayed in the trending graphs.
795 :param normalize: If True, the data in graphs is normalized.
796 :param url: An URL to be displayed in the modal window.
798 :type normalize: bool
800 :returns: A collumn with trending graphs (tput and latency) in tabs.
806 figs = graph_trending(self._data, tests, self._graph_layout, normalize)
814 id={"type": "graph", "index": "tput"},
826 id={"type": "graph", "index": "lat"},
839 active_tab="tab-tput",
852 "text-transform": "none",
853 "padding": "0rem 1rem"
858 dbc.ModalHeader(dbc.ModalTitle("URL")),
867 id="plot-btn-download",
868 children="Download Data",
872 "text-transform": "none",
873 "padding": "0rem 1rem"
876 dcc.Download(id="download-trending-data")
879 "d-grid gap-0 d-md-flex justify-content-md-end"
896 class_name="g-0 p-1",
897 start_collapsed=False,
899 active_item=["item-0", ]
901 class_name="g-0 p-0",
906 def _get_plotting_area_telemetry(self, graphs: list) -> dbc.Col:
907 """Generate the plotting area with telemetry.
916 title=f"Telemetry: {graph[1]}",
918 id={"type": "graph-telemetry", "index": graph[1]},
929 class_name="g-0 p-1",
930 start_collapsed=False,
932 active_item=[f"item-{i}" for i in range(len(acc_items))]
934 class_name="g-0 p-0",
940 def _get_telemetry_step_1() -> list:
941 """Return the content of the modal window used in the step 1 of metrics
944 :returns: A list of dbc rows with 'input' and 'search output'.
949 class_name="g-0 p-1",
952 id="telemetry-search-in",
953 placeholder="Start typing a metric name...",
959 class_name="g-0 p-1",
962 class_name="overflow-auto p-0",
963 id="telemetry-search-out",
965 style={"max-height": "14em"},
973 def _get_telemetry_step_2() -> list:
974 """Return the content of the modal window used in the step 2 of metrics
977 :returns: A list of dbc rows with 'container with dynamic dropdowns' and
984 class_name="g-0 p-1",
985 children=["Add content here."]
988 class_name="g-0 p-1",
991 id="telemetry-list-metrics",
1001 def callbacks(self, app):
1002 """Callbacks for the whole application.
1004 :param app: The application.
1010 Output("store-control-panel", "data"),
1011 Output("store-selected-tests", "data"),
1012 Output("plotting-area-trending", "children"),
1013 Output("plotting-area-buttons", "children"),
1014 Output("row-card-sel-tests", "style"),
1015 Output("row-btns-sel-tests", "style"),
1016 Output("lg-selected", "children"),
1017 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
1018 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
1019 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
1020 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
1021 Output({"type": "ctrl-dd", "index": "area"}, "options"),
1022 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
1023 Output({"type": "ctrl-dd", "index": "area"}, "value"),
1024 Output({"type": "ctrl-dd", "index": "test"}, "options"),
1025 Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
1026 Output({"type": "ctrl-dd", "index": "test"}, "value"),
1027 Output({"type": "ctrl-cl", "index": "core"}, "options"),
1028 Output({"type": "ctrl-cl", "index": "core"}, "value"),
1029 Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
1030 Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
1031 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
1032 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
1033 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
1034 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
1035 Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
1036 Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
1037 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
1038 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
1039 Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
1040 Output("normalize", "value")
1043 State("store-control-panel", "data"),
1044 State("store-selected-tests", "data"),
1045 State({"type": "sel-cl", "index": ALL}, "value")
1048 Input("url", "href"),
1049 Input("normalize", "value"),
1050 Input({"type": "ctrl-dd", "index": ALL}, "value"),
1051 Input({"type": "ctrl-cl", "index": ALL}, "value"),
1052 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
1054 prevent_initial_call=True
1056 def _update_application(
1057 control_panel: dict,
1064 """Update the application when the event is detected.
1067 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
1071 parsed_url = url_decode(href)
1073 url_params = parsed_url["params"]
1077 trigger = Trigger(callback_context.triggered)
1079 if trigger.type == "url" and url_params:
1081 store_sel = literal_eval(url_params["store_sel"][0])
1082 normalize = literal_eval(url_params["norm"][0])
1083 except (KeyError, IndexError):
1086 last_test = store_sel[-1]
1087 test = self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1088 [last_test["area"]][last_test["test"]]
1090 "dd-dut-val": last_test["dut"],
1091 "dd-phy-val": last_test["phy"],
1092 "dd-phy-opt": generate_options(
1093 self._spec_tbs[last_test["dut"]].keys()
1095 "dd-phy-dis": False,
1096 "dd-area-val": last_test["area"],
1098 {"label": label(v), "value": v} for v in sorted(
1099 self._spec_tbs[last_test["dut"]]\
1100 [last_test["phy"]].keys()
1103 "dd-area-dis": False,
1104 "dd-test-val": last_test["test"],
1105 "dd-test-opt": generate_options(
1106 self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1107 [last_test["area"]].keys()
1109 "dd-test-dis": False,
1110 "cl-core-opt": generate_options(test["core"]),
1111 "cl-core-val": [last_test["core"].upper(), ],
1112 "cl-core-all-val": list(),
1113 "cl-core-all-opt": C.CL_ALL_ENABLED,
1114 "cl-frmsize-opt": generate_options(test["frame-size"]),
1115 "cl-frmsize-val": [last_test["framesize"].upper(), ],
1116 "cl-frmsize-all-val": list(),
1117 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1118 "cl-tsttype-opt": generate_options(test["test-type"]),
1119 "cl-tsttype-val": [last_test["testtype"].upper(), ],
1120 "cl-tsttype-all-val": list(),
1121 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1122 "cl-normalize-val": normalize,
1123 "btn-add-dis": False
1126 elif trigger.type == "normalize":
1127 ctrl_panel.set({"cl-normalize-val": normalize})
1129 elif trigger.type == "ctrl-dd":
1130 if trigger.idx == "dut":
1132 options = generate_options(
1133 self._spec_tbs[trigger.value].keys()
1140 "dd-dut-val": trigger.value,
1141 "dd-phy-val": str(),
1142 "dd-phy-opt": options,
1143 "dd-phy-dis": disabled,
1144 "dd-area-val": str(),
1145 "dd-area-opt": list(),
1146 "dd-area-dis": True,
1147 "dd-test-val": str(),
1148 "dd-test-opt": list(),
1149 "dd-test-dis": True,
1150 "cl-core-opt": list(),
1151 "cl-core-val": list(),
1152 "cl-core-all-val": list(),
1153 "cl-core-all-opt": C.CL_ALL_DISABLED,
1154 "cl-frmsize-opt": list(),
1155 "cl-frmsize-val": list(),
1156 "cl-frmsize-all-val": list(),
1157 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1158 "cl-tsttype-opt": list(),
1159 "cl-tsttype-val": list(),
1160 "cl-tsttype-all-val": list(),
1161 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1164 elif trigger.idx == "phy":
1166 dut = ctrl_panel.get("dd-dut-val")
1167 phy = self._spec_tbs[dut][trigger.value]
1168 options = [{"label": label(v), "value": v} \
1169 for v in sorted(phy.keys())]
1175 "dd-phy-val": trigger.value,
1176 "dd-area-val": str(),
1177 "dd-area-opt": options,
1178 "dd-area-dis": disabled,
1179 "dd-test-val": str(),
1180 "dd-test-opt": list(),
1181 "dd-test-dis": True,
1182 "cl-core-opt": list(),
1183 "cl-core-val": list(),
1184 "cl-core-all-val": list(),
1185 "cl-core-all-opt": C.CL_ALL_DISABLED,
1186 "cl-frmsize-opt": list(),
1187 "cl-frmsize-val": list(),
1188 "cl-frmsize-all-val": list(),
1189 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1190 "cl-tsttype-opt": list(),
1191 "cl-tsttype-val": list(),
1192 "cl-tsttype-all-val": list(),
1193 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1196 elif trigger.idx == "area":
1198 dut = ctrl_panel.get("dd-dut-val")
1199 phy = ctrl_panel.get("dd-phy-val")
1200 area = self._spec_tbs[dut][phy][trigger.value]
1201 options = generate_options(area.keys())
1207 "dd-area-val": trigger.value,
1208 "dd-test-val": str(),
1209 "dd-test-opt": options,
1210 "dd-test-dis": disabled,
1211 "cl-core-opt": list(),
1212 "cl-core-val": list(),
1213 "cl-core-all-val": list(),
1214 "cl-core-all-opt": C.CL_ALL_DISABLED,
1215 "cl-frmsize-opt": list(),
1216 "cl-frmsize-val": list(),
1217 "cl-frmsize-all-val": list(),
1218 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1219 "cl-tsttype-opt": list(),
1220 "cl-tsttype-val": list(),
1221 "cl-tsttype-all-val": list(),
1222 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1225 elif trigger.idx == "test":
1226 dut = ctrl_panel.get("dd-dut-val")
1227 phy = ctrl_panel.get("dd-phy-val")
1228 area = ctrl_panel.get("dd-area-val")
1229 if all((dut, phy, area, trigger.value, )):
1230 test = self._spec_tbs[dut][phy][area][trigger.value]
1232 "dd-test-val": trigger.value,
1233 "cl-core-opt": generate_options(test["core"]),
1234 "cl-core-val": list(),
1235 "cl-core-all-val": list(),
1236 "cl-core-all-opt": C.CL_ALL_ENABLED,
1238 generate_options(test["frame-size"]),
1239 "cl-frmsize-val": list(),
1240 "cl-frmsize-all-val": list(),
1241 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1243 generate_options(test["test-type"]),
1244 "cl-tsttype-val": list(),
1245 "cl-tsttype-all-val": list(),
1246 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1249 elif trigger.type == "ctrl-cl":
1250 param = trigger.idx.split("-")[0]
1251 if "-all" in trigger.idx:
1252 c_sel, c_all, c_id = list(), trigger.value, "all"
1254 c_sel, c_all, c_id = trigger.value, list(), str()
1255 val_sel, val_all = sync_checklists(
1256 options=ctrl_panel.get(f"cl-{param}-opt"),
1262 f"cl-{param}-val": val_sel,
1263 f"cl-{param}-all-val": val_all,
1265 if all((ctrl_panel.get("cl-core-val"),
1266 ctrl_panel.get("cl-frmsize-val"),
1267 ctrl_panel.get("cl-tsttype-val"), )):
1268 ctrl_panel.set({"btn-add-dis": False})
1270 ctrl_panel.set({"btn-add-dis": True})
1271 elif trigger.type == "ctrl-btn":
1273 if trigger.idx == "add-test":
1274 dut = ctrl_panel.get("dd-dut-val")
1275 phy = ctrl_panel.get("dd-phy-val")
1276 area = ctrl_panel.get("dd-area-val")
1277 test = ctrl_panel.get("dd-test-val")
1278 # Add selected test(s) to the list of tests in store:
1279 if store_sel is None:
1281 for core in ctrl_panel.get("cl-core-val"):
1282 for framesize in ctrl_panel.get("cl-frmsize-val"):
1283 for ttype in ctrl_panel.get("cl-tsttype-val"):
1288 phy.replace('af_xdp', 'af-xdp'),
1295 if tid not in [i["id"] for i in store_sel]:
1302 "framesize": framesize.lower(),
1303 "core": core.lower(),
1304 "testtype": ttype.lower()
1306 store_sel = sorted(store_sel, key=lambda d: d["id"])
1307 if C.CLEAR_ALL_INPUTS:
1308 ctrl_panel.set(ctrl_panel.defaults)
1309 elif trigger.idx == "rm-test" and lst_sel:
1310 new_store_sel = list()
1311 for idx, item in enumerate(store_sel):
1312 if not lst_sel[idx]:
1313 new_store_sel.append(item)
1314 store_sel = new_store_sel
1315 elif trigger.idx == "rm-test-all":
1320 lg_selected = get_list_group_items(store_sel, "sel-cl")
1321 plotting_area_trending = self._get_plotting_area_trending(
1326 {"store_sel": store_sel, "norm": normalize}
1329 plotting_area_buttons = self._get_plotting_area_buttons()
1330 row_card_sel_tests = C.STYLE_ENABLED
1331 row_btns_sel_tests = C.STYLE_ENABLED
1333 plotting_area_trending = C.PLACEHOLDER
1334 plotting_area_buttons = C.PLACEHOLDER
1335 row_card_sel_tests = C.STYLE_DISABLED
1336 row_btns_sel_tests = C.STYLE_DISABLED
1337 lg_selected = no_update
1340 plotting_area_trending = no_update
1341 plotting_area_buttons = no_update
1342 row_card_sel_tests = no_update
1343 row_btns_sel_tests = no_update
1344 lg_selected = no_update
1349 plotting_area_trending,
1350 plotting_area_buttons,
1355 ret_val.extend(ctrl_panel.values)
1359 Output("plot-mod-url", "is_open"),
1360 Input("plot-btn-url", "n_clicks"),
1361 State("plot-mod-url", "is_open")
1363 def toggle_plot_mod_url(n, is_open):
1364 """Toggle the modal window with url.
1371 Output("store-telemetry-data", "data"),
1372 Output("store-telemetry-user", "data"),
1373 Output("telemetry-search-in", "value"),
1374 Output("telemetry-search-out", "children"),
1375 Output("telemetry-list-metrics", "value"),
1376 Output("telemetry-dd", "children"),
1377 Output("plotting-area-telemetry", "children"),
1378 Output("plot-mod-telemetry-1", "is_open"),
1379 Output("plot-mod-telemetry-2", "is_open"),
1380 Output({"type": "telemetry-btn", "index": "select"}, "disabled"),
1381 Output({"type": "telemetry-btn", "index": "add"}, "disabled"),
1382 State("store-telemetry-data", "data"),
1383 State("store-telemetry-user", "data"),
1384 State("store-selected-tests", "data"),
1385 Input({"type": "tele-cl", "index": ALL}, "value"),
1386 Input("telemetry-search-in", "value"),
1387 Input({"type": "telemetry-btn", "index": ALL}, "n_clicks"),
1388 Input({"type": "tm-dd", "index": ALL}, "value"),
1389 prevent_initial_call=True
1391 def _update_plot_mod_telemetry(
1400 """Toggle the modal window with telemetry.
1403 if not any(n_clicks):
1407 # Telemetry user data
1408 # The data provided by user or result of user action
1410 # List of unique metrics:
1411 "unique_metrics": list(),
1412 # List of metrics selected by user:
1413 "selected_metrics": list(),
1414 # Labels from metrics selected by user (key: label name,
1415 # value: list of all possible values):
1416 "unique_labels": dict(),
1417 # Labels selected by the user (subset of 'unique_labels'):
1418 "selected_labels": dict(),
1419 # All unique metrics with labels (output from the step 1)
1420 # converted from pandas dataframe to dictionary.
1421 "unique_metrics_with_labels": dict(),
1422 # Metrics with labels selected by the user using dropdowns.
1423 "selected_metrics_with_labels": dict()
1426 tm = TelemetryData(tests=store_sel)
1428 search_out = no_update
1429 list_metrics = no_update
1431 plotting_area_telemetry = no_update
1432 is_open = (False, False)
1433 is_btn_disabled = (True, True)
1435 trigger = Trigger(callback_context.triggered)
1436 if trigger.type == "telemetry-btn":
1437 if trigger.idx in ("open", "back"):
1438 tm.from_dataframe(self._data)
1439 tm_json = tm.to_json()
1440 tm_user["unique_metrics"] = tm.unique_metrics
1441 tm_user["selected_metrics"] = list()
1442 tm_user["unique_labels"] = dict()
1443 tm_user["selected_labels"] = dict()
1445 search_out = get_list_group_items(
1446 tm_user["unique_metrics"],
1450 is_open = (True, False)
1451 elif trigger.idx == "select":
1452 tm.from_json(tm_data)
1454 if not tm_user["selected_metrics"]:
1455 tm_user["selected_metrics"] = \
1456 tm_user["unique_metrics"]
1457 metrics = [a for a, b in \
1458 zip(tm_user["selected_metrics"], cl_metrics) if b]
1459 tm_user["selected_metrics"] = metrics
1460 tm_user["unique_labels"] = \
1461 tm.get_selected_labels(metrics)
1462 tm_user["unique_metrics_with_labels"] = \
1463 tm.unique_metrics_with_labels
1464 list_metrics = tm.str_metrics
1465 tm_dd = _get_dd_container(tm_user["unique_labels"])
1467 is_btn_disabled = (True, False)
1468 is_open = (False, True)
1471 is_open = (False, False)
1472 elif trigger.idx == "add":
1473 tm.from_json(tm_data)
1474 plotting_area_telemetry = self._get_plotting_area_telemetry(
1476 tm.select_tm_trending_data(
1477 tm_user["selected_metrics_with_labels"]
1482 is_open = (False, False)
1483 elif trigger.idx == "cancel":
1485 is_open = (False, False)
1486 elif trigger.type == "telemetry-search-in":
1487 tm.from_metrics(tm_user["unique_metrics"])
1488 tm_user["selected_metrics"] = \
1489 tm.search_unique_metrics(search_in)
1490 search_out = get_list_group_items(
1491 tm_user["selected_metrics"],
1495 is_open = (True, False)
1496 elif trigger.type == "tele-cl":
1498 is_btn_disabled = (False, True)
1499 is_open = (True, False)
1500 elif trigger.type == "tm-dd":
1501 tm.from_metrics_with_labels(
1502 tm_user["unique_metrics_with_labels"]
1506 for itm in tm_dd_in:
1509 elif isinstance(itm, str):
1511 selected[itm] = list()
1512 elif isinstance(itm, list):
1513 if previous_itm is not None:
1514 selected[previous_itm] = itm
1518 tm_dd = _get_dd_container(
1519 tm_user["unique_labels"],
1523 sel_metrics = tm.filter_selected_metrics_by_labels(selected)
1524 tm_user["selected_metrics_with_labels"] = sel_metrics.to_dict()
1525 if not sel_metrics.empty:
1526 list_metrics = tm.metrics_to_str(sel_metrics)
1528 list_metrics = str()
1530 is_btn_disabled = (True, False)
1531 is_open = (False, True)
1541 plotting_area_telemetry
1543 ret_val.extend(is_open)
1544 ret_val.extend(is_btn_disabled)
1547 def _get_dd_container(
1549 selected_labels: dict=dict(),
1552 """Generate a container with dropdown selection boxes depenting on
1555 :param all_labels: A dictionary with unique labels and their
1557 :param selected_labels: A dictionalry with user selected lables and
1559 :param show_new: If True, a dropdown selection box to add a new
1561 :type all_labels: dict
1562 :type selected_labels: dict
1563 :type show_new: bool
1564 :returns: A list of dbc rows with dropdown selection boxes.
1575 """Generates a dbc row with dropdown boxes.
1577 :param id: A string added to the dropdown ID.
1578 :param lopts: A list of options for 'label' dropdown.
1579 :param lval: Value of 'label' dropdown.
1580 :param vopts: A list of options for 'value' dropdown.
1581 :param vvals: A list of values for 'value' dropdown.
1587 :returns: dbc row with dropdown boxes.
1599 "index": f"label-{id}"
1601 placeholder="Select a label...",
1605 value=lval if lval else None
1618 "index": f"value-{id}"
1620 placeholder="Select a value...",
1624 value=vvals if vvals else None
1630 return dbc.Row(class_name="g-0 p-1", children=children)
1634 # Display rows with items in 'selected_labels'; label on the left,
1635 # values on the right:
1636 keys_left = list(all_labels.keys())
1637 for idx, label in enumerate(selected_labels.keys()):
1638 container.append(_row(
1640 lopts=deepcopy(keys_left),
1642 vopts=all_labels[label],
1643 vvals=selected_labels[label]
1645 keys_left.remove(label)
1647 # Display row with dd with labels on the left, right side is empty:
1648 if show_new and keys_left:
1649 container.append(_row(id="new", lopts=keys_left))
1654 Output("metadata-tput-lat", "children"),
1655 Output("metadata-hdrh-graph", "children"),
1656 Output("offcanvas-metadata", "is_open"),
1657 Input({"type": "graph", "index": ALL}, "clickData"),
1658 prevent_initial_call=True
1660 def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1661 """Generates the data for the offcanvas displayed when a particular
1662 point in a graph is clicked on.
1664 :param graph_data: The data from the clicked point in the graph.
1665 :type graph_data: dict
1666 :returns: The data to be displayed on the offcanvas and the
1667 information to show the offcanvas.
1668 :rtype: tuple(list, list, bool)
1671 trigger = Trigger(callback_context.triggered)
1674 idx = 0 if trigger.idx == "tput" else 1
1675 graph_data = graph_data[idx]["points"][0]
1676 except (IndexError, KeyError, ValueError, TypeError):
1679 metadata = no_update
1684 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1685 ) for x in graph_data.get("text", "").split("<br>")
1687 if trigger.idx == "tput":
1688 title = "Throughput"
1689 elif trigger.idx == "lat":
1691 hdrh_data = graph_data.get("customdata", None)
1694 class_name="gy-2 p-0",
1696 dbc.CardHeader(hdrh_data.pop("name")),
1697 dbc.CardBody(children=[
1699 id="hdrh-latency-graph",
1700 figure=graph_hdrh_latency(
1701 hdrh_data, self._graph_layout
1712 class_name="gy-2 p-0",
1714 dbc.CardHeader(children=[
1716 target_id="tput-lat-metadata",
1718 style={"display": "inline-block"}
1723 id="tput-lat-metadata",
1725 children=[dbc.ListGroup(children, flush=True), ]
1731 return metadata, graph, True
1734 Output("download-trending-data", "data"),
1735 State("store-selected-tests", "data"),
1736 Input("plot-btn-download", "n_clicks"),
1737 prevent_initial_call=True
1739 def _download_trending_data(store_sel: list, _) -> dict:
1740 """Download the data
1742 :param store_sel: List of tests selected by user stored in the
1744 :type store_sel: list
1745 :returns: dict of data frame content (base64 encoded) and meta data
1746 used by the Download component.
1754 for itm in store_sel:
1755 sel_data = select_trending_data(self._data, itm)
1756 if sel_data is None:
1758 df = pd.concat([df, sel_data], ignore_index=True, copy=False)
1760 return dcc.send_data_frame(df.to_csv, C.TREND_DOWNLOAD_FILE_NAME)