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(infra, None) is None:
146 tbs[dut][infra] = dict()
147 if tbs[dut][infra].get(area, None) is None:
148 tbs[dut][infra][area] = dict()
149 if tbs[dut][infra][area].get(test, None) is None:
150 tbs[dut][infra][area][test] = dict()
151 tbs[dut][infra][area][test]["core"] = list()
152 tbs[dut][infra][area][test]["frame-size"] = list()
153 tbs[dut][infra][area][test]["test-type"] = list()
154 if core.upper() not in tbs[dut][infra][area][test]["core"]:
155 tbs[dut][infra][area][test]["core"].append(core.upper())
156 if framesize.upper() not in \
157 tbs[dut][infra][area][test]["frame-size"]:
158 tbs[dut][infra][area][test]["frame-size"].append(
161 if row["test_type"] == "mrr":
162 if "MRR" not in tbs[dut][infra][area][test]["test-type"]:
163 tbs[dut][infra][area][test]["test-type"].append("MRR")
164 elif row["test_type"] == "ndrpdr":
165 if "NDR" not in tbs[dut][infra][area][test]["test-type"]:
166 tbs[dut][infra][area][test]["test-type"].extend(
169 elif row["test_type"] == "hoststack":
170 if row["tg_type"] in ("iperf", "vpp"):
171 if "BPS" not in tbs[dut][infra][area][test]["test-type"]:
172 tbs[dut][infra][area][test]["test-type"].append("BPS")
173 elif row["tg_type"] == "ab":
174 if "CPS" not in tbs[dut][infra][area][test]["test-type"]:
175 tbs[dut][infra][area][test]["test-type"].extend(
181 self._html_layout = str()
182 self._graph_layout = None
183 self._tooltips = dict()
186 with open(self._html_layout_file, "r") as file_read:
187 self._html_layout = file_read.read()
188 except IOError as err:
190 f"Not possible to open the file {self._html_layout_file}\n{err}"
194 with open(self._graph_layout_file, "r") as file_read:
195 self._graph_layout = load(file_read, Loader=FullLoader)
196 except IOError as err:
198 f"Not possible to open the file {self._graph_layout_file}\n"
201 except YAMLError as err:
203 f"An error occurred while parsing the specification file "
204 f"{self._graph_layout_file}\n{err}"
208 with open(self._tooltip_file, "r") as file_read:
209 self._tooltips = load(file_read, Loader=FullLoader)
210 except IOError as err:
212 f"Not possible to open the file {self._tooltip_file}\n{err}"
214 except YAMLError as err:
216 f"An error occurred while parsing the specification file "
217 f"{self._tooltip_file}\n{err}"
221 if self._app is not None and hasattr(self, "callbacks"):
222 self.callbacks(self._app)
225 def html_layout(self):
226 return self._html_layout
228 def add_content(self):
229 """Top level method which generated the web page.
232 - Store for user input data,
234 - Main area with control panel and ploting area.
236 If no HTML layout is provided, an error message is displayed instead.
238 :returns: The HTML div with the whole page.
242 if self.html_layout and self._spec_tbs:
247 dcc.Store(id="store"),
248 dcc.Location(id="url", refresh=False),
260 self._add_ctrl_col(),
261 self._add_plotting_col()
267 id="offcanvas-metadata",
268 title="Throughput And Latency",
272 dbc.Row(id="metadata-tput-lat"),
273 dbc.Row(id="metadata-hdrh-graph")
276 delay_show=C.SPINNER_DELAY
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(
300 id="navbarsimple-main",
313 brand_external_link=True,
318 def _add_ctrl_col(self) -> dbc.Col:
319 """Add column with controls. It is placed on the left side.
321 :returns: Column with the control panel.
326 children=self._add_ctrl_panel(),
327 className="sticky-top"
331 def _add_ctrl_panel(self) -> list:
332 """Add control panel.
334 :returns: Control panel.
339 class_name="g-0 p-1",
344 children=show_tooltip(
351 id={"type": "ctrl-dd", "index": "dut"},
352 placeholder="Select a Device under Test...",
355 {"label": k, "value": k} \
356 for k in self._spec_tbs.keys()
358 key=lambda d: d["label"]
367 class_name="g-0 p-1",
372 children=show_tooltip(
379 id={"type": "ctrl-dd", "index": "phy"},
381 "Select a Physical Test Bed Topology..."
389 class_name="g-0 p-1",
394 children=show_tooltip(
401 id={"type": "ctrl-dd", "index": "area"},
402 placeholder="Select an Area..."
410 class_name="g-0 p-1",
415 children=show_tooltip(
422 id={"type": "ctrl-dd", "index": "test"},
423 placeholder="Select a Test..."
431 class_name="g-0 p-1",
436 children=show_tooltip(
447 "index": "frmsize-all"
449 options=C.CL_ALL_DISABLED,
468 style={"align-items": "center"},
474 class_name="g-0 p-1",
479 children=show_tooltip(
492 options=C.CL_ALL_DISABLED,
511 style={"align-items": "center"},
517 class_name="g-0 p-1",
522 children=show_tooltip(
533 "index": "tsttype-all"
535 options=C.CL_ALL_DISABLED,
554 style={"align-items": "center"},
560 class_name="g-0 p-1",
565 children=show_tooltip(
576 "value": "normalize",
578 "Normalize to CPU frequency "
589 style={"align-items": "center"},
595 class_name="g-0 p-1",
598 id={"type": "ctrl-btn", "index": "add-test"},
599 children="Add Selected",
605 id="row-card-sel-tests",
606 class_name="g-0 p-1",
607 style=C.STYLE_DISABLED,
610 class_name="overflow-auto p-0",
613 style={"max-height": "20em"},
619 id="row-btns-sel-tests",
620 class_name="g-0 p-1",
621 style=C.STYLE_DISABLED,
626 id={"type": "ctrl-btn", "index": "rm-test"},
627 children="Remove Selected",
633 id={"type": "ctrl-btn", "index": "rm-test-all"},
634 children="Remove All",
644 id="row-btns-add-tm",
645 class_name="g-0 p-1",
646 style=C.STYLE_DISABLED,
650 "Add Telemetry Panel",
651 id={"type": "telemetry-btn", "index": "open"},
654 dbc.Button("Show URL", id="plot-btn-url", color="info"),
657 dbc.ModalHeader(dbc.ModalTitle("URL")),
658 dbc.ModalBody(id="mod-url")
669 def _add_plotting_col(self) -> dbc.Col:
670 """Add column with plots. It is placed on the right side.
672 :returns: Column with plots.
676 id="col-plotting-area",
680 id="plotting-area-trending",
681 class_name="g-0 p-0",
682 children=C.PLACEHOLDER
684 delay_show=C.SPINNER_DELAY
687 id="plotting-area-telemetry",
688 class_name="g-0 p-0",
689 children=C.PLACEHOLDER
693 style=C.STYLE_DISABLED,
697 def _plotting_area_trending(graphs: list) -> dbc.Col:
698 """Generate the plotting area with all its content.
700 :param graphs: A list of graphs to be displayed in the trending page.
702 :returns: A collumn with trending graphs (tput and latency) in tabs.
714 id={"type": "graph", "index": "tput"},
726 id={"type": "graph", "index": "lat"},
739 active_tab="tab-tput",
748 id="plot-btn-download",
751 style={"padding": "0rem 1rem"}
753 dcc.Download(id="download-trending-data")
755 className="d-grid gap-0 d-md-flex justify-content-md-end"
764 dbc.AccordionItem(trending, title="Trending"),
765 class_name="g-0 p-1",
766 start_collapsed=False,
768 active_item=["item-0", ]
773 dbc.ModalTitle("Select a Metric"),
777 dbc.ModalBody(Layout._get_telemetry_step_1()),
778 delay_show=2 * C.SPINNER_DELAY
783 id={"type": "telemetry-btn", "index": "select"},
789 id={"type": "telemetry-btn", "index": "cancel"},
795 id={"type": "telemetry-btn", "index": "rm-all"},
801 id={"type": "plot-mod-telemetry", "index": 0},
811 dbc.ModalTitle("Select Labels"),
815 dbc.ModalBody(Layout._get_telemetry_step_2()),
816 delay_show=2 * C.SPINNER_DELAY
821 id={"type": "telemetry-btn", "index": "back"},
826 "Add Telemetry Panel",
827 id={"type": "telemetry-btn", "index": "add"},
833 id={"type": "telemetry-btn", "index": "cancel"},
839 id={"type": "plot-mod-telemetry", "index": 1},
850 def _plotting_area_telemetry(graphs: list) -> dbc.Col:
851 """Generate the plotting area with telemetry.
853 :param graphs: A list of graphs to be displayed in the telemetry page.
855 :returns: A collumn with telemetry trending graphs.
861 def _plural(iterative):
862 return "s" if len(iterative) > 1 else str()
865 for idx, graph_set in enumerate(graphs):
867 for graph in graph_set[0]:
868 graph_name = ", ".join(graph[1])
872 id={"type": "graph-telemetry", "index": graph_name},
875 title=(f"Test{_plural(graph[1])}: {graph_name}"),
885 class_name="g-0 p-0",
886 start_collapsed=True,
898 "type": "tm-btn-remove",
903 style={"padding": "0rem 1rem"}
908 "type": "tm-btn-download",
913 style={"padding": "0rem 1rem"}
917 "d-grid gap-0 d-md-flex justify-content-md-end"
922 class_name="g-0 p-0",
924 f"Metric{_plural(graph_set[1])}: ",
925 ", ".join(graph_set[1])
933 class_name="g-0 p-1",
934 start_collapsed=True,
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={"type": "telemetry-search-in", "index": 0},
953 placeholder="Start typing a metric name...",
959 class_name="g-0 p-1",
962 class_name="overflow-auto p-0",
963 id={"type": "telemetry-search-out", "index": 0},
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
983 id={"type": "tm-container", "index": 0},
984 class_name="g-0 p-1",
985 children=["Add content here."]
988 class_name="g-0 p-2",
991 id={"type": "cb-all-in-one", "index": 0},
992 label="All Metrics in one Graph"
997 class_name="g-0 p-1",
1000 id={"type": "tm-list-metrics", "index": 0},
1010 def callbacks(self, app):
1011 """Callbacks for the whole application.
1013 :param app: The application.
1018 Output("store", "data"),
1019 Output("plotting-area-trending", "children"),
1020 Output("plotting-area-telemetry", "children"),
1021 Output("col-plotting-area", "style"),
1022 Output("row-card-sel-tests", "style"),
1023 Output("row-btns-sel-tests", "style"),
1024 Output("row-btns-add-tm", "style"),
1025 Output("lg-selected", "children"),
1026 Output({"type": "telemetry-search-out", "index": ALL}, "children"),
1027 Output({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
1028 Output({"type": "telemetry-btn", "index": ALL}, "disabled"),
1029 Output({"type": "tm-container", "index": ALL}, "children"),
1030 Output({"type": "tm-list-metrics", "index": ALL}, "value"),
1031 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
1032 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
1033 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
1034 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
1035 Output({"type": "ctrl-dd", "index": "area"}, "options"),
1036 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
1037 Output({"type": "ctrl-dd", "index": "area"}, "value"),
1038 Output({"type": "ctrl-dd", "index": "test"}, "options"),
1039 Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
1040 Output({"type": "ctrl-dd", "index": "test"}, "value"),
1041 Output({"type": "ctrl-cl", "index": "core"}, "options"),
1042 Output({"type": "ctrl-cl", "index": "core"}, "value"),
1043 Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
1044 Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
1045 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
1046 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
1047 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
1048 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
1049 Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
1050 Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
1051 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
1052 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
1053 Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
1054 Output("normalize", "value"),
1056 State("store", "data"),
1057 State({"type": "sel-cl", "index": ALL}, "value"),
1058 State({"type": "cb-all-in-one", "index": ALL}, "value"),
1059 State({"type": "telemetry-search-out", "index": ALL}, "children"),
1060 State({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
1061 State({"type": "telemetry-btn", "index": ALL}, "disabled"),
1062 State({"type": "tm-container", "index": ALL}, "children"),
1063 State({"type": "tm-list-metrics", "index": ALL}, "value"),
1064 State({"type": "tele-cl", "index": ALL}, "value"),
1066 Input("url", "href"),
1067 Input({"type": "tm-dd", "index": ALL}, "value"),
1069 Input("normalize", "value"),
1070 Input({"type": "telemetry-search-in", "index": ALL}, "value"),
1071 Input({"type": "telemetry-btn", "index": ALL}, "n_clicks"),
1072 Input({"type": "tm-btn-remove", "index": ALL}, "n_clicks"),
1073 Input({"type": "ctrl-dd", "index": ALL}, "value"),
1074 Input({"type": "ctrl-cl", "index": ALL}, "value"),
1075 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks"),
1077 prevent_initial_call=True
1079 def _update_application(
1085 tm_btns_disabled: list,
1093 """Update the application when the event is detected.
1098 "control-panel": dict(),
1099 "selected-tests": list(),
1100 "trending-graphs": None,
1101 "telemetry-data": dict(),
1102 "selected-metrics": dict(),
1103 "telemetry-panels": list(),
1104 "telemetry-all-in-one": list(),
1105 "telemetry-graphs": list(),
1109 ctrl_panel = ControlPanel(
1111 store.get("control-panel", dict())
1113 store_sel = store["selected-tests"]
1114 tm_data = store["telemetry-data"]
1115 tm_user = store["selected-metrics"]
1116 tm_panels = store["telemetry-panels"]
1117 tm_all_in_one = store["telemetry-all-in-one"]
1119 plotting_area_telemetry = no_update
1120 on_draw = [False, False] # 0 --> trending, 1 --> telemetry
1123 parsed_url = url_decode(href)
1125 url_params = parsed_url["params"]
1130 # Telemetry user data
1131 # The data provided by user or result of user action
1133 # List of unique metrics:
1134 "unique_metrics": list(),
1135 # List of metrics selected by user:
1136 "selected_metrics": list(),
1137 # Labels from metrics selected by user (key: label name,
1138 # value: list of all possible values):
1139 "unique_labels": dict(),
1140 # Labels selected by the user (subset of 'unique_labels'):
1141 "selected_labels": dict(),
1142 # All unique metrics with labels (output from the step 1)
1143 # converted from pandas dataframe to dictionary.
1144 "unique_metrics_with_labels": dict(),
1145 # Metrics with labels selected by the user using dropdowns.
1146 "selected_metrics_with_labels": dict()
1148 tm = TelemetryData(store_sel) if store_sel else TelemetryData()
1150 trigger = Trigger(callback_context.triggered)
1151 if trigger.type == "url" and url_params:
1154 store_sel = literal_eval(url_params["store_sel"][0])
1155 normalize = literal_eval(url_params["norm"][0])
1156 telemetry = literal_eval(url_params["telemetry"][0])
1157 tm_all_in_one = literal_eval(url_params["all-in-one"][0])
1158 if not isinstance(telemetry, list):
1159 telemetry = [telemetry, ]
1160 tm_all_in_one = [tm_all_in_one, ]
1161 except (KeyError, IndexError, AttributeError, ValueError):
1164 last_test = store_sel[-1]
1165 test = self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1166 [last_test["area"]][last_test["test"]]
1168 "dd-dut-val": last_test["dut"],
1169 "dd-phy-val": last_test["phy"],
1170 "dd-phy-opt": generate_options(
1171 self._spec_tbs[last_test["dut"]].keys()
1173 "dd-phy-dis": False,
1174 "dd-area-val": last_test["area"],
1176 {"label": label(v), "value": v} for v in sorted(
1177 self._spec_tbs[last_test["dut"]]\
1178 [last_test["phy"]].keys()
1181 "dd-area-dis": False,
1182 "dd-test-val": last_test["test"],
1183 "dd-test-opt": generate_options(
1184 self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1185 [last_test["area"]].keys()
1187 "dd-test-dis": False,
1188 "cl-core-opt": generate_options(test["core"]),
1189 "cl-core-val": [last_test["core"].upper(), ],
1190 "cl-core-all-val": list(),
1191 "cl-core-all-opt": C.CL_ALL_ENABLED,
1192 "cl-frmsize-opt": generate_options(test["frame-size"]),
1193 "cl-frmsize-val": [last_test["framesize"].upper(), ],
1194 "cl-frmsize-all-val": list(),
1195 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1196 "cl-tsttype-opt": generate_options(test["test-type"]),
1197 "cl-tsttype-val": [last_test["testtype"].upper(), ],
1198 "cl-tsttype-all-val": list(),
1199 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1200 "cl-normalize-val": normalize,
1201 "btn-add-dis": False
1203 store["trending-graphs"] = None
1204 store["telemetry-graphs"] = list()
1207 tm = TelemetryData(store_sel)
1208 tm.from_dataframe(self._data)
1209 tm_data = tm.to_json()
1210 tm.from_json(tm_data)
1211 tm_panels = telemetry
1213 elif trigger.type == "normalize":
1214 ctrl_panel.set({"cl-normalize-val": trigger.value})
1215 store["trending-graphs"] = None
1217 elif trigger.type == "ctrl-dd":
1218 if trigger.idx == "dut":
1220 options = generate_options(
1221 self._spec_tbs[trigger.value].keys()
1228 "dd-dut-val": trigger.value,
1229 "dd-phy-val": str(),
1230 "dd-phy-opt": options,
1231 "dd-phy-dis": disabled,
1232 "dd-area-val": str(),
1233 "dd-area-opt": list(),
1234 "dd-area-dis": True,
1235 "dd-test-val": str(),
1236 "dd-test-opt": list(),
1237 "dd-test-dis": True,
1238 "cl-core-opt": list(),
1239 "cl-core-val": list(),
1240 "cl-core-all-val": list(),
1241 "cl-core-all-opt": C.CL_ALL_DISABLED,
1242 "cl-frmsize-opt": list(),
1243 "cl-frmsize-val": list(),
1244 "cl-frmsize-all-val": list(),
1245 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1246 "cl-tsttype-opt": list(),
1247 "cl-tsttype-val": list(),
1248 "cl-tsttype-all-val": list(),
1249 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1252 elif trigger.idx == "phy":
1254 dut = ctrl_panel.get("dd-dut-val")
1255 phy = self._spec_tbs[dut][trigger.value]
1256 options = [{"label": label(v), "value": v} \
1257 for v in sorted(phy.keys())]
1263 "dd-phy-val": trigger.value,
1264 "dd-area-val": str(),
1265 "dd-area-opt": options,
1266 "dd-area-dis": disabled,
1267 "dd-test-val": str(),
1268 "dd-test-opt": list(),
1269 "dd-test-dis": True,
1270 "cl-core-opt": list(),
1271 "cl-core-val": list(),
1272 "cl-core-all-val": list(),
1273 "cl-core-all-opt": C.CL_ALL_DISABLED,
1274 "cl-frmsize-opt": list(),
1275 "cl-frmsize-val": list(),
1276 "cl-frmsize-all-val": list(),
1277 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1278 "cl-tsttype-opt": list(),
1279 "cl-tsttype-val": list(),
1280 "cl-tsttype-all-val": list(),
1281 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1284 elif trigger.idx == "area":
1286 dut = ctrl_panel.get("dd-dut-val")
1287 phy = ctrl_panel.get("dd-phy-val")
1288 area = self._spec_tbs[dut][phy][trigger.value]
1289 options = generate_options(area.keys())
1295 "dd-area-val": trigger.value,
1296 "dd-test-val": str(),
1297 "dd-test-opt": options,
1298 "dd-test-dis": disabled,
1299 "cl-core-opt": list(),
1300 "cl-core-val": list(),
1301 "cl-core-all-val": list(),
1302 "cl-core-all-opt": C.CL_ALL_DISABLED,
1303 "cl-frmsize-opt": list(),
1304 "cl-frmsize-val": list(),
1305 "cl-frmsize-all-val": list(),
1306 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1307 "cl-tsttype-opt": list(),
1308 "cl-tsttype-val": list(),
1309 "cl-tsttype-all-val": list(),
1310 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1313 elif trigger.idx == "test":
1314 dut = ctrl_panel.get("dd-dut-val")
1315 phy = ctrl_panel.get("dd-phy-val")
1316 area = ctrl_panel.get("dd-area-val")
1317 if all((dut, phy, area, trigger.value, )):
1318 test = self._spec_tbs[dut][phy][area][trigger.value]
1320 "dd-test-val": trigger.value,
1321 "cl-core-opt": generate_options(test["core"]),
1322 "cl-core-val": list(),
1323 "cl-core-all-val": list(),
1324 "cl-core-all-opt": C.CL_ALL_ENABLED,
1326 generate_options(test["frame-size"]),
1327 "cl-frmsize-val": list(),
1328 "cl-frmsize-all-val": list(),
1329 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1331 generate_options(test["test-type"]),
1332 "cl-tsttype-val": list(),
1333 "cl-tsttype-all-val": list(),
1334 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1337 elif trigger.type == "ctrl-cl":
1338 param = trigger.idx.split("-")[0]
1339 if "-all" in trigger.idx:
1340 c_sel, c_all, c_id = list(), trigger.value, "all"
1342 c_sel, c_all, c_id = trigger.value, list(), str()
1343 val_sel, val_all = sync_checklists(
1344 options=ctrl_panel.get(f"cl-{param}-opt"),
1350 f"cl-{param}-val": val_sel,
1351 f"cl-{param}-all-val": val_all,
1353 if all((ctrl_panel.get("cl-core-val"),
1354 ctrl_panel.get("cl-frmsize-val"),
1355 ctrl_panel.get("cl-tsttype-val"), )):
1356 ctrl_panel.set({"btn-add-dis": False})
1358 ctrl_panel.set({"btn-add-dis": True})
1359 elif trigger.type == "ctrl-btn":
1361 tm_all_in_one = list()
1362 store["trending-graphs"] = None
1363 store["telemetry-graphs"] = list()
1365 on_draw = [True, True]
1366 if trigger.idx == "add-test":
1367 dut = ctrl_panel.get("dd-dut-val")
1368 phy = ctrl_panel.get("dd-phy-val")
1369 area = ctrl_panel.get("dd-area-val")
1370 test = ctrl_panel.get("dd-test-val")
1371 # Add selected test(s) to the list of tests in store:
1372 if store_sel is None:
1374 for core in ctrl_panel.get("cl-core-val"):
1375 for framesize in ctrl_panel.get("cl-frmsize-val"):
1376 for ttype in ctrl_panel.get("cl-tsttype-val"):
1381 phy.replace('af_xdp', 'af-xdp'),
1388 if tid not in [i["id"] for i in store_sel]:
1395 "framesize": framesize.lower(),
1396 "core": core.lower(),
1397 "testtype": ttype.lower()
1399 store_sel = sorted(store_sel, key=lambda d: d["id"])
1400 if C.CLEAR_ALL_INPUTS:
1401 ctrl_panel.set(ctrl_panel.defaults)
1402 elif trigger.idx == "rm-test" and lst_sel:
1403 new_store_sel = list()
1404 for idx, item in enumerate(store_sel):
1405 if not lst_sel[idx]:
1406 new_store_sel.append(item)
1407 store_sel = new_store_sel
1408 elif trigger.idx == "rm-test-all":
1410 elif trigger.type == "telemetry-btn":
1411 if trigger.idx in ("open", "back"):
1412 tm.from_dataframe(self._data)
1413 tm_data = tm.to_json()
1414 tm_user["unique_metrics"] = tm.unique_metrics
1415 tm_user["selected_metrics"] = list()
1416 tm_user["unique_labels"] = dict()
1417 tm_user["selected_labels"] = dict()
1419 get_list_group_items(tm_user["unique_metrics"],
1422 is_open = (True, False)
1423 tm_btns_disabled[1], tm_btns_disabled[5] = False, True
1424 elif trigger.idx == "select":
1426 tm.from_json(tm_data)
1427 if not tm_user["selected_metrics"]:
1428 tm_user["selected_metrics"] = \
1429 tm_user["unique_metrics"]
1430 metrics = [a for a, b in \
1431 zip(tm_user["selected_metrics"], cl_metrics) if b]
1432 tm_user["selected_metrics"] = metrics
1433 tm_user["unique_labels"] = \
1434 tm.get_selected_labels(metrics)
1435 tm_user["unique_metrics_with_labels"] = \
1436 tm.unique_metrics_with_labels
1437 list_metrics[0] = tm.str_metrics
1438 tm_dd[0] = _get_dd_container(tm_user["unique_labels"])
1440 tm_btns_disabled[1] = True
1441 tm_btns_disabled[4] = False
1442 is_open = (False, True)
1444 is_open = (True, False)
1445 elif trigger.idx == "add":
1446 tm.from_json(tm_data)
1447 tm_panels.append(tm_user["selected_metrics_with_labels"])
1448 tm_all_in_one.append(all_in_one)
1449 is_open = (False, False)
1450 tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1451 on_draw = [True, True]
1452 elif trigger.idx == "cancel":
1453 is_open = (False, False)
1454 tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1455 elif trigger.idx == "rm-all":
1457 tm_all_in_one = list()
1459 is_open = (False, False)
1460 tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1461 plotting_area_telemetry = C.PLACEHOLDER
1462 elif trigger.type == "telemetry-search-in":
1463 tm.from_metrics(tm_user["unique_metrics"])
1464 tm_user["selected_metrics"] = \
1465 tm.search_unique_metrics(trigger.value)
1466 search_out = (get_list_group_items(
1467 tm_user["selected_metrics"],
1471 is_open = (True, False)
1472 elif trigger.type == "tm-dd":
1473 tm.from_metrics_with_labels(
1474 tm_user["unique_metrics_with_labels"]
1478 for itm in tm_dd_in:
1481 elif isinstance(itm, str):
1483 selected[itm] = list()
1484 elif isinstance(itm, list):
1485 if previous_itm is not None:
1486 selected[previous_itm] = itm
1489 tm_dd[0] = _get_dd_container(
1490 tm_user["unique_labels"],
1494 sel_metrics = tm.filter_selected_metrics_by_labels(selected)
1495 tm_user["selected_metrics_with_labels"] = sel_metrics.to_dict()
1496 if not sel_metrics.empty:
1497 list_metrics[0] = tm.metrics_to_str(sel_metrics)
1498 tm_btns_disabled[5] = False
1500 list_metrics[0] = str()
1501 elif trigger.type == "tm-btn-remove":
1502 del tm_panels[trigger.idx]
1503 del tm_all_in_one[trigger.idx]
1504 del store["telemetry-graphs"][trigger.idx]
1505 tm.from_json(tm_data)
1506 on_draw = [True, True]
1509 "store_sel": store_sel,
1510 "norm": ctrl_panel.get("cl-normalize-val")
1513 new_url_params["telemetry"] = tm_panels
1514 new_url_params["all-in-one"] = tm_all_in_one
1516 if on_draw[0]: # Trending
1518 lg_selected = get_list_group_items(store_sel, "sel-cl")
1519 if store["trending-graphs"]:
1520 graphs = store["trending-graphs"]
1522 graphs = graph_trending(
1526 bool(ctrl_panel.get("cl-normalize-val"))
1528 if graphs and graphs[0]:
1529 store["trending-graphs"] = graphs
1530 plotting_area_trending = \
1531 Layout._plotting_area_trending(graphs)
1534 start_idx = len(store["telemetry-graphs"])
1535 end_idx = len(tm_panels)
1537 plotting_area_telemetry = C.PLACEHOLDER
1538 elif on_draw[1] and (end_idx >= start_idx):
1539 for idx in range(start_idx, end_idx):
1540 store["telemetry-graphs"].append(graph_tm_trending(
1541 tm.select_tm_trending_data(tm_panels[idx]),
1543 bool(tm_all_in_one[idx][0])
1545 plotting_area_telemetry = \
1546 Layout._plotting_area_telemetry(
1547 store["telemetry-graphs"]
1549 col_plotting_area = C.STYLE_ENABLED
1550 row_card_sel_tests = C.STYLE_ENABLED
1551 row_btns_sel_tests = C.STYLE_ENABLED
1552 row_btns_add_tm = C.STYLE_ENABLED
1554 plotting_area_trending = no_update
1555 plotting_area_telemetry = C.PLACEHOLDER
1556 col_plotting_area = C.STYLE_DISABLED
1557 row_card_sel_tests = C.STYLE_DISABLED
1558 row_btns_sel_tests = C.STYLE_DISABLED
1559 row_btns_add_tm = C.STYLE_DISABLED
1560 lg_selected = no_update
1563 tm_all_in_one = list()
1566 plotting_area_trending = no_update
1567 col_plotting_area = no_update
1568 row_card_sel_tests = no_update
1569 row_btns_sel_tests = no_update
1570 row_btns_add_tm = no_update
1571 lg_selected = no_update
1573 store["url"] = gen_new_url(parsed_url, new_url_params)
1574 store["control-panel"] = ctrl_panel.panel
1575 store["selected-tests"] = store_sel
1576 store["telemetry-data"] = tm_data
1577 store["selected-metrics"] = tm_user
1578 store["telemetry-panels"] = tm_panels
1579 store["telemetry-all-in-one"] = tm_all_in_one
1582 plotting_area_trending,
1583 plotting_area_telemetry,
1595 ret_val.extend(ctrl_panel.values)
1599 Output("plot-mod-url", "is_open"),
1600 Output("mod-url", "children"),
1601 State("store", "data"),
1602 State("plot-mod-url", "is_open"),
1603 Input("plot-btn-url", "n_clicks")
1605 def toggle_plot_mod_url(store, is_open, n_clicks):
1606 """Toggle the modal window with url.
1612 return not is_open, store.get("url", str())
1613 return is_open, store["url"]
1615 def _get_dd_container(
1617 selected_labels: dict=dict(),
1620 """Generate a container with dropdown selection boxes depenting on
1623 :param all_labels: A dictionary with unique labels and their
1625 :param selected_labels: A dictionalry with user selected lables and
1627 :param show_new: If True, a dropdown selection box to add a new
1629 :type all_labels: dict
1630 :type selected_labels: dict
1631 :type show_new: bool
1632 :returns: A list of dbc rows with dropdown selection boxes.
1643 """Generates a dbc row with dropdown boxes.
1645 :param id: A string added to the dropdown ID.
1646 :param lopts: A list of options for 'label' dropdown.
1647 :param lval: Value of 'label' dropdown.
1648 :param vopts: A list of options for 'value' dropdown.
1649 :param vvals: A list of values for 'value' dropdown.
1655 :returns: dbc row with dropdown boxes.
1667 "index": f"label-{id}"
1669 placeholder="Select a label...",
1673 value=lval if lval else None
1686 "index": f"value-{id}"
1688 placeholder="Select a value...",
1692 value=vvals if vvals else None
1698 return dbc.Row(class_name="g-0 p-1", children=children)
1702 # Display rows with items in 'selected_labels'; label on the left,
1703 # values on the right:
1704 keys_left = list(all_labels.keys())
1705 for idx, label in enumerate(selected_labels.keys()):
1706 container.append(_row(
1708 lopts=deepcopy(keys_left),
1710 vopts=all_labels[label],
1711 vvals=selected_labels[label]
1713 keys_left.remove(label)
1715 # Display row with dd with labels on the left, right side is empty:
1716 if show_new and keys_left:
1717 container.append(_row(id="new", lopts=keys_left))
1722 Output("metadata-tput-lat", "children"),
1723 Output("metadata-hdrh-graph", "children"),
1724 Output("offcanvas-metadata", "is_open"),
1725 Input({"type": "graph", "index": ALL}, "clickData"),
1726 prevent_initial_call=True
1728 def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1729 """Generates the data for the offcanvas displayed when a particular
1730 point in a graph is clicked on.
1732 :param graph_data: The data from the clicked point in the graph.
1733 :type graph_data: dict
1734 :returns: The data to be displayed on the offcanvas and the
1735 information to show the offcanvas.
1736 :rtype: tuple(list, list, bool)
1739 trigger = Trigger(callback_context.triggered)
1742 idx = 0 if trigger.idx == "tput" else 1
1743 graph_data = graph_data[idx]["points"][0]
1744 except (IndexError, KeyError, ValueError, TypeError):
1747 metadata = no_update
1752 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1753 ) for x in graph_data.get("text", "").split("<br>")
1755 if trigger.idx == "tput":
1756 title = "Throughput"
1757 elif trigger.idx == "lat":
1759 hdrh_data = graph_data.get("customdata", None)
1762 class_name="gy-2 p-0",
1764 dbc.CardHeader(hdrh_data.pop("name")),
1765 dbc.CardBody(children=[
1767 id="hdrh-latency-graph",
1768 figure=graph_hdrh_latency(
1769 hdrh_data, self._graph_layout
1780 class_name="gy-2 p-0",
1782 dbc.CardHeader(children=[
1784 target_id="tput-lat-metadata",
1786 style={"display": "inline-block"}
1791 id="tput-lat-metadata",
1793 children=[dbc.ListGroup(children, flush=True), ]
1799 return metadata, graph, True
1802 Output("download-trending-data", "data"),
1803 State("store", "data"),
1804 Input("plot-btn-download", "n_clicks"),
1805 Input({"type": "tm-btn-download", "index": ALL}, "n_clicks"),
1806 prevent_initial_call=True
1808 def _download_data(store: list, *_) -> dict:
1809 """Download the data
1811 :param store_sel: List of tests selected by user stored in the
1813 :type store_sel: list
1814 :returns: dict of data frame content (base64 encoded) and meta data
1815 used by the Download component.
1821 if not store["selected-tests"]:
1826 trigger = Trigger(callback_context.triggered)
1827 if not trigger.value:
1830 if trigger.type == "plot-btn-download":
1832 for itm in store["selected-tests"]:
1833 sel_data = select_trending_data(self._data, itm)
1834 if sel_data is None:
1836 data.append(sel_data)
1837 df = pd.concat(data, ignore_index=True, copy=False)
1838 file_name = C.TREND_DOWNLOAD_FILE_NAME
1839 elif trigger.type == "tm-btn-download":
1840 tm = TelemetryData(store["selected-tests"])
1841 tm.from_json(store["telemetry-data"])
1842 df = tm.select_tm_trending_data(
1843 store["telemetry-panels"][trigger.idx]
1845 file_name = C.TELEMETRY_DOWNLOAD_FILE_NAME
1849 return dcc.send_data_frame(df.to_csv, file_name)