1 # Copyright (c) 2024 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
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, navbar_trending, \
38 show_trending_graph_data
39 from ..utils.url_processing import url_decode
40 from .graphs import graph_trending, select_trending_data, graph_tm_trending
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 cols = ["job", "test_id", "test_type", "tg_type"]
115 for _, row in self._data[cols].drop_duplicates().iterrows():
116 lst_job = row["job"].split("-")
118 tbed = "-".join(lst_job[-2:])
119 lst_test = row["test_id"].split(".")
123 area = ".".join(lst_test[3:-2])
124 suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\
127 nic = suite.split("-")[0]
128 for drv in C.DRIVERS:
134 test = test.replace(f"{drv}-", "")
138 infra = "-".join((tbed, nic, driver))
139 lst_test = test.split("-")
140 framesize = lst_test[0]
141 core = lst_test[1] if lst_test[1] else "8C"
142 test = "-".join(lst_test[2: -1])
144 if tbs.get(dut, None) is None:
146 if tbs[dut].get(area, None) is None:
147 tbs[dut][area] = dict()
148 if tbs[dut][area].get(test, None) is None:
149 tbs[dut][area][test] = dict()
150 if tbs[dut][area][test].get(infra, None) is None:
151 tbs[dut][area][test][infra] = {
153 "frame-size": list(),
156 tst_params = tbs[dut][area][test][infra]
157 if core.upper() not in tst_params["core"]:
158 tst_params["core"].append(core.upper())
159 if framesize.upper() not in tst_params["frame-size"]:
160 tst_params["frame-size"].append(framesize.upper())
161 if row["test_type"] == "ndrpdr":
162 if "NDR" not in tst_params["test-type"]:
163 tst_params["test-type"].extend(("NDR", "PDR"))
164 elif row["test_type"] == "hoststack":
165 if row["tg_type"] in ("iperf", "vpp"):
166 if "BPS" not in tst_params["test-type"]:
167 tst_params["test-type"].append("BPS")
168 elif row["tg_type"] == "ab":
169 if "CPS" not in tst_params["test-type"]:
170 tst_params["test-type"].extend(("CPS", "RPS"))
172 if row["test_type"].upper() not in tst_params["test-type"]:
173 tst_params["test-type"].append(row["test_type"].upper())
177 self._html_layout = str()
178 self._graph_layout = None
179 self._tooltips = dict()
182 with open(self._html_layout_file, "r") as file_read:
183 self._html_layout = file_read.read()
184 except IOError as err:
186 f"Not possible to open the file {self._html_layout_file}\n{err}"
190 with open(self._graph_layout_file, "r") as file_read:
191 self._graph_layout = load(file_read, Loader=FullLoader)
192 except IOError as err:
194 f"Not possible to open the file {self._graph_layout_file}\n"
197 except YAMLError as err:
199 f"An error occurred while parsing the specification file "
200 f"{self._graph_layout_file}\n{err}"
204 with open(self._tooltip_file, "r") as file_read:
205 self._tooltips = load(file_read, Loader=FullLoader)
206 except IOError as err:
208 f"Not possible to open the file {self._tooltip_file}\n{err}"
210 except YAMLError as err:
212 f"An error occurred while parsing the specification file "
213 f"{self._tooltip_file}\n{err}"
217 if self._app is not None and hasattr(self, "callbacks"):
218 self.callbacks(self._app)
221 def html_layout(self):
222 return self._html_layout
224 def add_content(self):
225 """Top level method which generated the web page.
228 - Store for user input data,
230 - Main area with control panel and ploting area.
232 If no HTML layout is provided, an error message is displayed instead.
234 :returns: The HTML div with the whole page.
238 if self.html_layout and self._spec_tbs:
243 dcc.Store(id="store"),
244 dcc.Location(id="url", refresh=False),
248 children=[navbar_trending((True, False, False, False))]
254 self._add_ctrl_col(),
255 self._add_plotting_col()
261 id="offcanvas-metadata",
262 title="Detailed Information",
266 dbc.Row(id="metadata-tput-lat"),
267 dbc.Row(id="metadata-hdrh-graph")
270 delay_show=C.SPINNER_DELAY
274 id="offcanvas-documentation",
275 title="Documentation",
278 children=html.Iframe(
279 src=C.URL_DOC_TRENDING,
288 dbc.Alert("An Error Occured", color="danger"),
292 def _add_ctrl_col(self) -> dbc.Col:
293 """Add column with controls. It is placed on the left side.
295 :returns: Column with the control panel.
298 return dbc.Col(html.Div(self._add_ctrl_panel(), className="sticky-top"))
300 def _add_ctrl_panel(self) -> list:
301 """Add control panel.
303 :returns: Control panel.
311 show_tooltip(self._tooltips, "help-dut", "DUT")
314 id={"type": "ctrl-dd", "index": "dut"},
315 placeholder="Select a Device under Test...",
318 {"label": k, "value": k} \
319 for k in self._spec_tbs.keys()
321 key=lambda d: d["label"]
333 show_tooltip(self._tooltips, "help-area", "Area")
336 id={"type": "ctrl-dd", "index": "area"},
337 placeholder="Select an Area..."
348 show_tooltip(self._tooltips, "help-test", "Test")
351 id={"type": "ctrl-dd", "index": "test"},
352 placeholder="Select a Test..."
363 show_tooltip(self._tooltips, "help-infra", "Infra")
366 id={"type": "ctrl-dd", "index": "phy"},
367 placeholder="Select a Physical Test Bed Topology..."
377 dbc.InputGroupText(show_tooltip(
384 id={"type": "ctrl-cl", "index": "frmsize-all"},
385 options=C.CL_ALL_DISABLED,
393 id={"type": "ctrl-cl", "index": "frmsize"},
398 style={"align-items": "center"},
406 dbc.InputGroupText(show_tooltip(
413 id={"type": "ctrl-cl", "index": "core-all"},
414 options=C.CL_ALL_DISABLED,
422 id={"type": "ctrl-cl", "index": "core"},
427 style={"align-items": "center"},
435 dbc.InputGroupText(show_tooltip(
442 id={"type": "ctrl-cl", "index": "tsttype-all"},
443 options=C.CL_ALL_DISABLED,
451 id={"type": "ctrl-cl", "index": "tsttype"},
456 style={"align-items": "center"},
464 dbc.InputGroupText(show_tooltip(
469 dbc.Col(dbc.Checklist(
472 "value": "normalize",
473 "label": "Normalize to CPU frequency 2GHz"
480 style={"align-items": "center"},
487 id={"type": "ctrl-btn", "index": "add-test"},
488 children="Add Selected",
495 class_name="overflow-auto p-0",
498 style={"max-height": "20em"},
501 id="row-card-sel-tests",
502 class_name="g-0 p-1",
503 style=C.STYLE_DISABLED,
509 id={"type": "ctrl-btn", "index": "rm-test"},
516 id={"type": "ctrl-btn", "index": "rm-test-all"},
522 id="row-btns-sel-tests",
523 class_name="g-0 p-1",
524 style=C.STYLE_DISABLED,
529 "Add Telemetry Panel",
530 id={"type": "telemetry-btn", "index": "open"},
533 dbc.Button("Show URL", id="plot-btn-url", color="info"),
536 dbc.ModalHeader(dbc.ModalTitle("URL")),
537 dbc.ModalBody(id="mod-url")
545 id="row-btns-add-tm",
546 class_name="g-0 p-1",
547 style=C.STYLE_DISABLED,
552 def _add_plotting_col(self) -> dbc.Col:
553 """Add column with plots. It is placed on the right side.
555 :returns: Column with plots.
559 id="col-plotting-area",
562 id="plotting-area-trending",
563 class_name="g-0 p-0",
564 children=C.PLACEHOLDER
567 id="plotting-area-telemetry",
568 class_name="g-0 p-0",
569 children=C.PLACEHOLDER
573 style=C.STYLE_DISABLED,
577 def _plotting_area_trending(graphs: list) -> dbc.Col:
578 """Generate the plotting area with all its content.
580 :param graphs: A list of graphs to be displayed in the trending page.
582 :returns: A collumn with trending graphs (tput and latency) in tabs.
594 id={"type": "graph", "index": "tput"},
606 id={"type": "graph", "index": "bandwidth"},
610 tab_id="tab-bandwidth"
618 id={"type": "graph", "index": "lat"},
631 active_tab="tab-tput",
640 id="plot-btn-download",
643 style={"padding": "0rem 1rem"}
645 dcc.Download(id="download-trending-data")
647 className="d-grid gap-0 d-md-flex justify-content-md-end"
656 dbc.AccordionItem(trending, title="Trending"),
657 class_name="g-0 p-1",
658 start_collapsed=False,
660 active_item=["item-0", ]
665 dbc.ModalTitle("Select a Metric"),
669 dbc.ModalBody(Layout._get_telemetry_step_1()),
670 delay_show=2 * C.SPINNER_DELAY
675 id={"type": "telemetry-btn", "index": "select"},
681 id={"type": "telemetry-btn", "index": "cancel"},
687 id={"type": "telemetry-btn", "index": "rm-all"},
693 id={"type": "plot-mod-telemetry", "index": 0},
703 dbc.ModalTitle("Select Labels"),
707 dbc.ModalBody(Layout._get_telemetry_step_2()),
708 delay_show=2 * C.SPINNER_DELAY
713 id={"type": "telemetry-btn", "index": "back"},
718 "Add Telemetry Panel",
719 id={"type": "telemetry-btn", "index": "add"},
725 id={"type": "telemetry-btn", "index": "cancel"},
731 id={"type": "plot-mod-telemetry", "index": 1},
742 def _plotting_area_telemetry(graphs: list) -> dbc.Col:
743 """Generate the plotting area with telemetry.
745 :param graphs: A list of graphs to be displayed in the telemetry page.
747 :returns: A collumn with telemetry trending graphs.
753 def _plural(iterative):
754 return "s" if len(iterative) > 1 else str()
757 for idx, graph_set in enumerate(graphs):
759 for graph in graph_set[0]:
760 graph_name = ", ".join(graph[1])
764 id={"type": "graph-telemetry", "index": graph_name},
767 title=(f"Test{_plural(graph[1])}: {graph_name}"),
777 class_name="g-0 p-0",
778 start_collapsed=True,
790 "type": "tm-btn-remove",
795 style={"padding": "0rem 1rem"}
800 "type": "tm-btn-download",
805 style={"padding": "0rem 1rem"}
809 "d-grid gap-0 d-md-flex justify-content-md-end"
814 class_name="g-0 p-0",
816 f"Metric{_plural(graph_set[1])}: ",
817 ", ".join(graph_set[1])
825 class_name="g-0 p-1",
826 start_collapsed=True,
832 def _get_telemetry_step_1() -> list:
833 """Return the content of the modal window used in the step 1 of metrics
836 :returns: A list of dbc rows with 'input' and 'search output'.
841 class_name="g-0 p-1",
844 id={"type": "telemetry-search-in", "index": 0},
845 placeholder="Start typing a metric name...",
851 class_name="g-0 p-1",
854 class_name="overflow-auto p-0",
855 id={"type": "telemetry-search-out", "index": 0},
857 style={"max-height": "14em"},
865 def _get_telemetry_step_2() -> list:
866 """Return the content of the modal window used in the step 2 of metrics
869 :returns: A list of dbc rows with 'container with dynamic dropdowns' and
876 id={"type": "tm-container", "index": 0},
883 id={"type": "cb-all-in-one", "index": 0},
884 label="All Metrics in one Graph"
890 id={"type": "cb-ignore-host", "index": 0},
900 id={"type": "tm-list-metrics", "index": 0},
910 def callbacks(self, app):
911 """Callbacks for the whole application.
913 :param app: The application.
918 Output("store", "data"),
919 Output("plotting-area-trending", "children"),
920 Output("plotting-area-telemetry", "children"),
921 Output("col-plotting-area", "style"),
922 Output("row-card-sel-tests", "style"),
923 Output("row-btns-sel-tests", "style"),
924 Output("row-btns-add-tm", "style"),
925 Output("lg-selected", "children"),
926 Output({"type": "telemetry-search-out", "index": ALL}, "children"),
927 Output({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
928 Output({"type": "telemetry-btn", "index": ALL}, "disabled"),
929 Output({"type": "tm-container", "index": ALL}, "children"),
930 Output({"type": "tm-list-metrics", "index": ALL}, "value"),
931 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
932 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
933 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
934 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
935 Output({"type": "ctrl-dd", "index": "area"}, "options"),
936 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
937 Output({"type": "ctrl-dd", "index": "area"}, "value"),
938 Output({"type": "ctrl-dd", "index": "test"}, "options"),
939 Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
940 Output({"type": "ctrl-dd", "index": "test"}, "value"),
941 Output({"type": "ctrl-cl", "index": "core"}, "options"),
942 Output({"type": "ctrl-cl", "index": "core"}, "value"),
943 Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
944 Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
945 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
946 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
947 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
948 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
949 Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
950 Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
951 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
952 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
953 Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
954 Output("normalize", "value"),
956 State("store", "data"),
957 State({"type": "sel-cl", "index": ALL}, "value"),
958 State({"type": "cb-all-in-one", "index": ALL}, "value"),
959 State({"type": "cb-ignore-host", "index": ALL}, "value"),
960 State({"type": "telemetry-search-out", "index": ALL}, "children"),
961 State({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
962 State({"type": "telemetry-btn", "index": ALL}, "disabled"),
963 State({"type": "tm-container", "index": ALL}, "children"),
964 State({"type": "tm-list-metrics", "index": ALL}, "value"),
965 State({"type": "tele-cl", "index": ALL}, "value"),
967 Input("url", "href"),
968 Input({"type": "tm-dd", "index": ALL}, "value"),
970 Input("normalize", "value"),
971 Input({"type": "telemetry-search-in", "index": ALL}, "value"),
972 Input({"type": "telemetry-btn", "index": ALL}, "n_clicks"),
973 Input({"type": "tm-btn-remove", "index": ALL}, "n_clicks"),
974 Input({"type": "ctrl-dd", "index": ALL}, "value"),
975 Input({"type": "ctrl-cl", "index": ALL}, "value"),
976 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks"),
978 prevent_initial_call=True
980 def _update_application(
987 tm_btns_disabled: list,
995 """Update the application when the event is detected.
1000 "control-panel": dict(),
1001 "selected-tests": list(),
1002 "trending-graphs": None,
1003 "telemetry-data": dict(),
1004 "selected-metrics": dict(),
1005 "telemetry-panels": list(),
1006 "telemetry-all-in-one": list(),
1007 "telemetry-ignore-host": list(),
1008 "telemetry-graphs": list(),
1012 ctrl_panel = ControlPanel(
1014 store.get("control-panel", dict())
1016 store_sel = store["selected-tests"]
1017 tm_data = store["telemetry-data"]
1018 tm_user = store["selected-metrics"]
1019 tm_panels = store["telemetry-panels"]
1020 tm_all_in_one = store["telemetry-all-in-one"]
1021 tm_ignore_host = store["telemetry-ignore-host"]
1023 plotting_area_telemetry = no_update
1024 on_draw = [False, False] # 0 --> trending, 1 --> telemetry
1027 parsed_url = url_decode(href)
1029 url_params = parsed_url["params"]
1034 # Telemetry user data
1035 # The data provided by user or result of user action
1037 # List of unique metrics:
1038 "unique_metrics": list(),
1039 # List of metrics selected by user:
1040 "selected_metrics": list(),
1041 # Labels from metrics selected by user (key: label name,
1042 # value: list of all possible values):
1043 "unique_labels": dict(),
1044 # Labels selected by the user (subset of 'unique_labels'):
1045 "selected_labels": dict(),
1046 # All unique metrics with labels (output from the step 1)
1047 # converted from pandas dataframe to dictionary.
1048 "unique_metrics_with_labels": dict(),
1049 # Metrics with labels selected by the user using dropdowns.
1050 "selected_metrics_with_labels": dict()
1052 tm = TelemetryData(store_sel) if store_sel else TelemetryData()
1054 trigger = Trigger(callback_context.triggered)
1055 if trigger.type == "url" and url_params:
1058 store_sel = literal_eval(url_params["store_sel"][0])
1059 normalize = literal_eval(url_params["norm"][0])
1060 telemetry = literal_eval(url_params["telemetry"][0])
1061 url_p = url_params.get("all-in-one", ["[[None]]"])
1062 tm_all_in_one = literal_eval(url_p[0])
1063 url_p = url_params.get("ignore-host", ["[[None]]"])
1064 tm_ignore_host = literal_eval(url_p[0])
1065 if not isinstance(telemetry, list):
1066 telemetry = [telemetry, ]
1067 except (KeyError, IndexError, AttributeError, ValueError):
1070 last_test = store_sel[-1]
1071 test = self._spec_tbs[last_test["dut"]]\
1072 [last_test["area"]][last_test["test"]][last_test["phy"]]
1074 "dd-dut-val": last_test["dut"],
1075 "dd-area-val": last_test["area"],
1077 {"label": label(v), "value": v} for v in sorted(
1078 self._spec_tbs[last_test["dut"]].keys())
1080 "dd-area-dis": False,
1081 "dd-test-val": last_test["test"],
1082 "dd-test-opt": generate_options(
1083 self._spec_tbs[last_test["dut"]]\
1084 [last_test["area"]].keys()
1086 "dd-test-dis": False,
1087 "dd-phy-val": last_test["phy"],
1088 "dd-phy-opt": generate_options(
1089 self._spec_tbs[last_test["dut"]][last_test["area"]]\
1090 [last_test["test"]].keys()
1092 "dd-phy-dis": False,
1093 "cl-core-opt": generate_options(test["core"]),
1094 "cl-core-val": [last_test["core"].upper(), ],
1095 "cl-core-all-val": list(),
1096 "cl-core-all-opt": C.CL_ALL_ENABLED,
1097 "cl-frmsize-opt": generate_options(test["frame-size"]),
1098 "cl-frmsize-val": [last_test["framesize"].upper(), ],
1099 "cl-frmsize-all-val": list(),
1100 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1101 "cl-tsttype-opt": generate_options(test["test-type"]),
1102 "cl-tsttype-val": [last_test["testtype"].upper(), ],
1103 "cl-tsttype-all-val": list(),
1104 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1105 "cl-normalize-val": normalize,
1106 "btn-add-dis": False
1108 store["trending-graphs"] = None
1109 store["telemetry-graphs"] = list()
1112 tm = TelemetryData(store_sel)
1113 tm.from_dataframe(self._data)
1114 tm_data = tm.to_json()
1115 tm.from_json(tm_data)
1116 tm_panels = telemetry
1118 elif trigger.type == "normalize":
1119 ctrl_panel.set({"cl-normalize-val": trigger.value})
1120 store["trending-graphs"] = None
1122 elif trigger.type == "ctrl-dd":
1123 if trigger.idx == "dut":
1125 dut = self._spec_tbs[trigger.value]
1126 options = [{"label": label(v), "value": v} \
1127 for v in sorted(dut.keys())]
1133 "dd-dut-val": trigger.value,
1134 "dd-area-val": str(),
1135 "dd-area-opt": options,
1136 "dd-area-dis": disabled,
1137 "dd-test-val": str(),
1138 "dd-test-opt": list(),
1139 "dd-test-dis": True,
1140 "dd-phy-val": str(),
1141 "dd-phy-opt": list(),
1143 "cl-core-opt": list(),
1144 "cl-core-val": list(),
1145 "cl-core-all-val": list(),
1146 "cl-core-all-opt": C.CL_ALL_DISABLED,
1147 "cl-frmsize-opt": list(),
1148 "cl-frmsize-val": list(),
1149 "cl-frmsize-all-val": list(),
1150 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1151 "cl-tsttype-opt": list(),
1152 "cl-tsttype-val": list(),
1153 "cl-tsttype-all-val": list(),
1154 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1157 if trigger.idx == "area":
1159 dut = ctrl_panel.get("dd-dut-val")
1160 area = self._spec_tbs[dut][trigger.value]
1161 options = generate_options(area.keys())
1167 "dd-area-val": trigger.value,
1168 "dd-test-val": str(),
1169 "dd-test-opt": options,
1170 "dd-test-dis": disabled,
1171 "dd-phy-val": str(),
1172 "dd-phy-opt": list(),
1174 "cl-core-opt": list(),
1175 "cl-core-val": list(),
1176 "cl-core-all-val": list(),
1177 "cl-core-all-opt": C.CL_ALL_DISABLED,
1178 "cl-frmsize-opt": list(),
1179 "cl-frmsize-val": list(),
1180 "cl-frmsize-all-val": list(),
1181 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1182 "cl-tsttype-opt": list(),
1183 "cl-tsttype-val": list(),
1184 "cl-tsttype-all-val": list(),
1185 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1188 if trigger.idx == "test":
1190 dut = ctrl_panel.get("dd-dut-val")
1191 area = ctrl_panel.get("dd-area-val")
1192 test = self._spec_tbs[dut][area][trigger.value]
1193 options = generate_options(test.keys())
1199 "dd-test-val": trigger.value,
1200 "dd-phy-val": str(),
1201 "dd-phy-opt": options,
1202 "dd-phy-dis": disabled,
1203 "cl-core-opt": list(),
1204 "cl-core-val": list(),
1205 "cl-core-all-val": list(),
1206 "cl-core-all-opt": C.CL_ALL_DISABLED,
1207 "cl-frmsize-opt": list(),
1208 "cl-frmsize-val": list(),
1209 "cl-frmsize-all-val": list(),
1210 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1211 "cl-tsttype-opt": list(),
1212 "cl-tsttype-val": list(),
1213 "cl-tsttype-all-val": list(),
1214 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1217 if trigger.idx == "phy":
1218 dut = ctrl_panel.get("dd-dut-val")
1219 area = ctrl_panel.get("dd-area-val")
1220 test = ctrl_panel.get("dd-test-val")
1221 if all((dut, area, test, trigger.value, )):
1222 phy = self._spec_tbs[dut][area][test][trigger.value]
1224 "dd-phy-val": trigger.value,
1225 "cl-core-opt": generate_options(phy["core"]),
1226 "cl-core-val": list(),
1227 "cl-core-all-val": list(),
1228 "cl-core-all-opt": C.CL_ALL_ENABLED,
1230 generate_options(phy["frame-size"]),
1231 "cl-frmsize-val": list(),
1232 "cl-frmsize-all-val": list(),
1233 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1235 generate_options(phy["test-type"]),
1236 "cl-tsttype-val": list(),
1237 "cl-tsttype-all-val": list(),
1238 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1241 elif trigger.type == "ctrl-cl":
1242 param = trigger.idx.split("-")[0]
1243 if "-all" in trigger.idx:
1244 c_sel, c_all, c_id = list(), trigger.value, "all"
1246 c_sel, c_all, c_id = trigger.value, list(), str()
1247 val_sel, val_all = sync_checklists(
1248 options=ctrl_panel.get(f"cl-{param}-opt"),
1254 f"cl-{param}-val": val_sel,
1255 f"cl-{param}-all-val": val_all,
1257 if all((ctrl_panel.get("cl-core-val"),
1258 ctrl_panel.get("cl-frmsize-val"),
1259 ctrl_panel.get("cl-tsttype-val"), )):
1260 ctrl_panel.set({"btn-add-dis": False})
1262 ctrl_panel.set({"btn-add-dis": True})
1263 elif trigger.type == "ctrl-btn":
1265 tm_all_in_one = list()
1266 tm_ignore_host = list()
1267 store["trending-graphs"] = None
1268 store["telemetry-graphs"] = list()
1269 on_draw = [True, True]
1270 if trigger.idx == "add-test":
1271 dut = ctrl_panel.get("dd-dut-val")
1272 phy = ctrl_panel.get("dd-phy-val")
1273 area = ctrl_panel.get("dd-area-val")
1274 test = ctrl_panel.get("dd-test-val")
1275 # Add selected test(s) to the list of tests in store:
1276 if store_sel is None:
1278 for core in ctrl_panel.get("cl-core-val"):
1279 for framesize in ctrl_panel.get("cl-frmsize-val"):
1280 for ttype in ctrl_panel.get("cl-tsttype-val"):
1285 phy.replace('af_xdp', 'af-xdp'),
1292 if tid not in [i["id"] for i in store_sel]:
1299 "framesize": framesize.lower(),
1300 "core": core.lower(),
1301 "testtype": ttype.lower()
1303 store_sel = sorted(store_sel, key=lambda d: d["id"])
1304 if C.CLEAR_ALL_INPUTS:
1305 ctrl_panel.set(ctrl_panel.defaults)
1306 elif trigger.idx == "rm-test" and lst_sel:
1307 new_store_sel = list()
1308 for idx, item in enumerate(store_sel):
1309 if not lst_sel[idx]:
1310 new_store_sel.append(item)
1311 store_sel = new_store_sel
1312 elif trigger.idx == "rm-test-all":
1314 elif trigger.type == "telemetry-btn":
1315 if trigger.idx in ("open", "back"):
1316 tm.from_dataframe(self._data)
1317 tm_data = tm.to_json()
1318 tm_user["unique_metrics"] = tm.unique_metrics
1319 tm_user["selected_metrics"] = list()
1320 tm_user["unique_labels"] = dict()
1321 tm_user["selected_labels"] = dict()
1323 get_list_group_items(tm_user["unique_metrics"],
1326 is_open = (True, False)
1327 tm_btns_disabled[1], tm_btns_disabled[5] = False, True
1328 elif trigger.idx == "select":
1330 tm.from_json(tm_data)
1331 if not tm_user["selected_metrics"]:
1332 tm_user["selected_metrics"] = \
1333 tm_user["unique_metrics"]
1334 metrics = [a for a, b in \
1335 zip(tm_user["selected_metrics"], cl_metrics) if b]
1336 tm_user["selected_metrics"] = metrics
1337 tm_user["unique_labels"] = \
1338 tm.get_selected_labels(metrics)
1339 tm_user["unique_metrics_with_labels"] = \
1340 tm.unique_metrics_with_labels
1341 list_metrics[0] = tm.str_metrics
1342 tm_dd[0] = _get_dd_container(tm_user["unique_labels"])
1344 tm_btns_disabled[1] = True
1345 tm_btns_disabled[4] = False
1346 is_open = (False, True)
1348 is_open = (True, False)
1349 elif trigger.idx == "add":
1350 tm.from_json(tm_data)
1351 tm_panels.append(tm_user["selected_metrics_with_labels"])
1352 tm_all_in_one.append(all_in_one)
1353 tm_ignore_host.append(ignore_host)
1354 is_open = (False, False)
1355 tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1356 on_draw = [True, True]
1357 elif trigger.idx == "cancel":
1358 is_open = (False, False)
1359 tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1360 elif trigger.idx == "rm-all":
1362 tm_all_in_one = list()
1363 tm_ignore_host = list()
1365 is_open = (False, False)
1366 tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1367 plotting_area_telemetry = C.PLACEHOLDER
1368 elif trigger.type == "telemetry-search-in":
1369 tm.from_metrics(tm_user["unique_metrics"])
1370 tm_user["selected_metrics"] = \
1371 tm.search_unique_metrics(trigger.value)
1372 search_out = (get_list_group_items(
1373 tm_user["selected_metrics"],
1377 is_open = (True, False)
1378 elif trigger.type == "tm-dd":
1379 tm.from_metrics_with_labels(
1380 tm_user["unique_metrics_with_labels"]
1384 for itm in tm_dd_in:
1387 elif isinstance(itm, str):
1389 selected[itm] = list()
1390 elif isinstance(itm, list):
1391 if previous_itm is not None:
1392 selected[previous_itm] = itm
1395 tm_dd[0] = _get_dd_container(
1396 tm_user["unique_labels"],
1400 sel_metrics = tm.filter_selected_metrics_by_labels(selected)
1401 tm_user["selected_metrics_with_labels"] = sel_metrics.to_dict()
1402 if not sel_metrics.empty:
1403 list_metrics[0] = tm.metrics_to_str(sel_metrics)
1404 tm_btns_disabled[5] = False
1406 list_metrics[0] = str()
1407 elif trigger.type == "tm-btn-remove":
1408 del tm_panels[trigger.idx]
1409 del tm_all_in_one[trigger.idx]
1410 del tm_ignore_host[trigger.idx]
1411 del store["telemetry-graphs"][trigger.idx]
1412 tm.from_json(tm_data)
1413 on_draw = [True, True]
1416 "store_sel": store_sel,
1417 "norm": ctrl_panel.get("cl-normalize-val")
1420 new_url_params["telemetry"] = tm_panels
1421 new_url_params["all-in-one"] = tm_all_in_one
1422 new_url_params["ignore-host"] = tm_ignore_host
1424 if on_draw[0]: # Trending
1426 lg_selected = get_list_group_items(store_sel, "sel-cl")
1427 if store["trending-graphs"]:
1428 graphs = store["trending-graphs"]
1430 graphs = graph_trending(
1434 bool(ctrl_panel.get("cl-normalize-val"))
1436 if graphs and graphs[0]:
1437 store["trending-graphs"] = graphs
1438 plotting_area_trending = \
1439 Layout._plotting_area_trending(graphs)
1442 start_idx = len(store["telemetry-graphs"])
1443 end_idx = len(tm_panels)
1445 plotting_area_telemetry = C.PLACEHOLDER
1446 elif on_draw[1] and (end_idx >= start_idx):
1447 if len(tm_all_in_one) != end_idx:
1448 tm_all_in_one = [[None], ] * end_idx
1449 if len(tm_ignore_host) != end_idx:
1450 tm_ignore_host = [[None], ] * end_idx
1451 for idx in range(start_idx, end_idx):
1452 store["telemetry-graphs"].append(graph_tm_trending(
1453 tm.select_tm_trending_data(
1455 ignore_host=bool(tm_ignore_host[idx][0])
1458 bool(tm_all_in_one[idx][0])
1460 plotting_area_telemetry = \
1461 Layout._plotting_area_telemetry(
1462 store["telemetry-graphs"]
1464 col_plotting_area = C.STYLE_ENABLED
1465 row_card_sel_tests = C.STYLE_ENABLED
1466 row_btns_sel_tests = C.STYLE_ENABLED
1467 row_btns_add_tm = C.STYLE_ENABLED
1469 plotting_area_trending = no_update
1470 plotting_area_telemetry = C.PLACEHOLDER
1471 col_plotting_area = C.STYLE_DISABLED
1472 row_card_sel_tests = C.STYLE_DISABLED
1473 row_btns_sel_tests = C.STYLE_DISABLED
1474 row_btns_add_tm = C.STYLE_DISABLED
1475 lg_selected = no_update
1478 tm_all_in_one = list()
1479 tm_ignore_host = list()
1482 plotting_area_trending = no_update
1483 col_plotting_area = no_update
1484 row_card_sel_tests = no_update
1485 row_btns_sel_tests = no_update
1486 row_btns_add_tm = no_update
1487 lg_selected = no_update
1489 store["url"] = gen_new_url(parsed_url, new_url_params)
1490 store["control-panel"] = ctrl_panel.panel
1491 store["selected-tests"] = store_sel
1492 store["telemetry-data"] = tm_data
1493 store["selected-metrics"] = tm_user
1494 store["telemetry-panels"] = tm_panels
1495 store["telemetry-all-in-one"] = tm_all_in_one
1496 store["telemetry-ignore-host"] = tm_ignore_host
1499 plotting_area_trending,
1500 plotting_area_telemetry,
1512 ret_val.extend(ctrl_panel.values)
1516 Output("plot-mod-url", "is_open"),
1517 Output("mod-url", "children"),
1518 State("store", "data"),
1519 State("plot-mod-url", "is_open"),
1520 Input("plot-btn-url", "n_clicks")
1522 def toggle_plot_mod_url(store, is_open, n_clicks):
1523 """Toggle the modal window with url.
1529 return not is_open, store.get("url", str())
1530 return is_open, store["url"]
1532 def _get_dd_container(
1534 selected_labels: dict=dict(),
1537 """Generate a container with dropdown selection boxes depenting on
1540 :param all_labels: A dictionary with unique labels and their
1542 :param selected_labels: A dictionalry with user selected lables and
1544 :param show_new: If True, a dropdown selection box to add a new
1546 :type all_labels: dict
1547 :type selected_labels: dict
1548 :type show_new: bool
1549 :returns: A list of dbc rows with dropdown selection boxes.
1560 """Generates a dbc row with dropdown boxes.
1562 :param id: A string added to the dropdown ID.
1563 :param lopts: A list of options for 'label' dropdown.
1564 :param lval: Value of 'label' dropdown.
1565 :param vopts: A list of options for 'value' dropdown.
1566 :param vvals: A list of values for 'value' dropdown.
1572 :returns: dbc row with dropdown boxes.
1584 "index": f"label-{id}"
1586 placeholder="Select a label...",
1590 value=lval if lval else None
1603 "index": f"value-{id}"
1605 placeholder="Select a value...",
1609 value=vvals if vvals else None
1615 return dbc.Row(class_name="g-0 p-1", children=children)
1619 # Display rows with items in 'selected_labels'; label on the left,
1620 # values on the right:
1621 keys_left = list(all_labels.keys())
1622 for idx, label in enumerate(selected_labels.keys()):
1623 container.append(_row(
1625 lopts=deepcopy(keys_left),
1627 vopts=all_labels[label],
1628 vvals=selected_labels[label]
1630 keys_left.remove(label)
1632 # Display row with dd with labels on the left, right side is empty:
1633 if show_new and keys_left:
1634 container.append(_row(id="new", lopts=keys_left))
1639 Output("metadata-tput-lat", "children"),
1640 Output("metadata-hdrh-graph", "children"),
1641 Output("offcanvas-metadata", "is_open"),
1642 Input({"type": "graph", "index": ALL}, "clickData"),
1643 prevent_initial_call=True
1645 def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1646 """Generates the data for the offcanvas displayed when a particular
1647 point in a graph is clicked on.
1649 :param graph_data: The data from the clicked point in the graph.
1650 :type graph_data: dict
1651 :returns: The data to be displayed on the offcanvas and the
1652 information to show the offcanvas.
1653 :rtype: tuple(list, list, bool)
1656 trigger = Trigger(callback_context.triggered)
1657 if not trigger.value:
1660 return show_trending_graph_data(
1661 trigger, graph_data, self._graph_layout)
1664 Output("download-trending-data", "data"),
1665 State("store", "data"),
1666 Input("plot-btn-download", "n_clicks"),
1667 Input({"type": "tm-btn-download", "index": ALL}, "n_clicks"),
1668 prevent_initial_call=True
1670 def _download_data(store: list, *_) -> dict:
1671 """Download the data
1673 :param store_sel: List of tests selected by user stored in the
1675 :type store_sel: list
1676 :returns: dict of data frame content (base64 encoded) and meta data
1677 used by the Download component.
1683 if not store["selected-tests"]:
1688 trigger = Trigger(callback_context.triggered)
1689 if not trigger.value:
1692 if trigger.type == "plot-btn-download":
1694 for itm in store["selected-tests"]:
1695 sel_data = select_trending_data(self._data, itm)
1696 if sel_data is None:
1698 data.append(sel_data)
1699 df = pd.concat(data, ignore_index=True, copy=False)
1700 file_name = C.TREND_DOWNLOAD_FILE_NAME
1701 elif trigger.type == "tm-btn-download":
1702 tm = TelemetryData(store["selected-tests"])
1703 tm.from_json(store["telemetry-data"])
1704 df = tm.select_tm_trending_data(
1705 store["telemetry-panels"][trigger.idx]
1707 file_name = C.TELEMETRY_DOWNLOAD_FILE_NAME
1711 return dcc.send_data_frame(df.to_csv, file_name)
1714 Output("offcanvas-documentation", "is_open"),
1715 Input("btn-documentation", "n_clicks"),
1716 State("offcanvas-documentation", "is_open")
1718 def toggle_offcanvas_documentation(n_clicks, is_open):