1 # Copyright (c) 2022 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.
14 """Plotly Dash HTML layout override.
21 from dash import callback_context, no_update
22 from dash import Input, Output, State
23 from dash.exceptions import PreventUpdate
24 import dash_bootstrap_components as dbc
25 from yaml import load, FullLoader, YAMLError
26 from datetime import datetime, timedelta
28 from ..data.data import Data
29 from .graphs import graph_trending, graph_hdrh_latency, \
37 STYLE_HIDEN = {"display": "none"}
38 STYLE_BLOCK = {"display": "block", "vertical-align": "top"}
40 "display": "inline-block",
41 "vertical-align": "top"
43 NO_GRAPH = {"data": [], "layout": {}, "frames": []}
45 def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
52 self._html_layout_file = html_layout_file
53 self._spec_file = spec_file
54 self._graph_layout_file = graph_layout_file
55 self._data_spec_file = data_spec_file
59 data_spec_file=self._data_spec_file,
64 data_spec_file=self._data_spec_file,
66 ).read_trending_ndrpdr()
68 self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
71 self._html_layout = ""
73 self._graph_layout = None
76 with open(self._html_layout_file, "r") as file_read:
77 self._html_layout = file_read.read()
78 except IOError as err:
80 f"Not possible to open the file {self._html_layout_file}\n{err}"
84 with open(self._spec_file, "r") as file_read:
85 self._spec_tbs = load(file_read, Loader=FullLoader)
86 except IOError as err:
88 f"Not possible to open the file {self._spec_file,}\n{err}"
90 except YAMLError as err:
92 f"An error occurred while parsing the specification file "
93 f"{self._spec_file,}\n"
98 with open(self._graph_layout_file, "r") as file_read:
99 self._graph_layout = load(file_read, Loader=FullLoader)
100 except IOError as err:
102 f"Not possible to open the file {self._graph_layout_file}\n"
105 except YAMLError as err:
107 f"An error occurred while parsing the specification file "
108 f"{self._graph_layout_file}\n"
113 if self._app is not None and hasattr(self, 'callbacks'):
114 self.callbacks(self._app)
117 def html_layout(self):
118 return self._html_layout
122 return self._spec_tbs
130 return self._graph_layout
132 def add_content(self):
135 if self.html_layout and self.spec_tbs:
139 dcc.Store(id="selected-tests"),
141 self._add_ctrl_div(),
142 self._add_plotting_div()
148 children="An Error Occured."
151 def _add_navbar(self):
152 """Add nav element with navigation panel. It is placed on the top.
154 return dbc.NavbarSimple(
157 dbc.NavLink("Continuous Performance Trending", href="#")
167 def _add_ctrl_div(self):
168 """Add div with controls. It is placed on the left side.
174 id="div-controls-tabs",
176 self._add_ctrl_select(),
177 self._add_ctrl_shown()
182 "display": "inline-block",
187 def _add_plotting_div(self):
188 """Add div with plots and tables. It is placed on the right side.
191 id="div-plotting-area",
193 html.Table(children=[
196 style=self.STYLE_HIDEN,
204 ], style={"width": "80%"}),
207 target_id="tput-metadata",
209 style={"display": "inline-block"}
214 children="**Throughput**",
215 style={"display": "inline-block"}
219 children="Click on data point in the graph"
222 id="div-lat-metadata",
223 style=self.STYLE_HIDEN,
226 target_id="lat-metadata",
228 style={"display": "inline-block"}
233 children="**Latency**",
234 style={"display": "inline-block"}
239 "Click on data point in the graph"
243 ], style={"width": "20%"}),
248 style=self.STYLE_HIDEN,
256 ], style={"width": "80%"}),
260 id="graph-latency-hdrh",
261 style=self.STYLE_INLINE,
265 ], style={"width": "20%"}),
270 style=self.STYLE_HIDEN,
276 id="btn-download-data",
277 children=["Download Data"]
279 dcc.Download(id="download-data")
282 ], style={"width": "80%"}),
285 ], style={"width": "20%"}),
291 "vertical-align": "top",
292 "display": "inline-block",
297 def _add_ctrl_shown(self):
303 html.H5("Selected tests"),
305 id="container-selected-tests",
310 labelStyle={"display": "block"}
314 children="Remove Selected",
318 id="btn-sel-display",
327 def _add_ctrl_select(self):
331 id="div-ctrl-select",
333 html.H5("Physical Test Bed Topology, NIC and Driver"),
336 placeholder="Select a Physical Test Bed Topology...",
340 {"label": k, "value": k} for k in self.spec_tbs.keys()
346 placeholder="Select an Area...",
354 placeholder="Select a Test...",
362 html.H5("Number of Cores"),
364 id="cl-ctrl-core-all",
365 options=[{"label": "All", "value": "all"}, ],
366 labelStyle={"display": "inline-block"}
370 labelStyle={"display": "inline-block"}
373 style={"display": "none"}
376 id="div-ctrl-framesize",
378 html.H5("Frame Size"),
380 id="cl-ctrl-framesize-all",
381 options=[{"label": "All", "value": "all"}, ],
382 labelStyle={"display": "inline-block"}
385 id="cl-ctrl-framesize",
386 labelStyle={"display": "inline-block"}
389 style={"display": "none"}
392 id="div-ctrl-testtype",
394 html.H5("Test Type"),
396 id="cl-ctrl-testtype-all",
397 options=[{"label": "All", "value": "all"}, ],
398 labelStyle={"display": "inline-block"}
401 id="cl-ctrl-testtype",
402 labelStyle={"display": "inline-block"}
405 style={"display": "none"}
415 min_date_allowed=datetime.utcnow() - timedelta(days=180),
416 max_date_allowed=datetime.utcnow(),
417 initial_visible_month=datetime.utcnow(),
418 start_date=datetime.utcnow() - timedelta(days=180),
419 end_date=datetime.utcnow(),
420 display_format="D MMMM YY"
425 def callbacks(self, app):
428 Output("dd-ctrl-area", "options"),
429 Output("dd-ctrl-area", "disabled"),
430 Input("dd-ctrl-phy", "value"),
432 def _update_dd_area(phy):
441 {"label": self.spec_tbs[phy][v]["label"], "value": v}
442 for v in [v for v in self.spec_tbs[phy].keys()]
449 return options, disable
452 Output("dd-ctrl-test", "options"),
453 Output("dd-ctrl-test", "disabled"),
454 State("dd-ctrl-phy", "value"),
455 Input("dd-ctrl-area", "value"),
457 def _update_dd_test(phy, area):
466 {"label": v, "value": v}
467 for v in self.spec_tbs[phy][area]["test"]
474 return options, disable
477 Output("div-ctrl-core", "style"),
478 Output("cl-ctrl-core", "options"),
479 Output("div-ctrl-framesize", "style"),
480 Output("cl-ctrl-framesize", "options"),
481 Output("div-ctrl-testtype", "style"),
482 Output("cl-ctrl-testtype", "options"),
483 Output("btn-ctrl-add", "disabled"),
484 State("dd-ctrl-phy", "value"),
485 State("dd-ctrl-area", "value"),
486 Input("dd-ctrl-test", "value"),
488 def _update_btn_add(phy, area, test):
495 core_style = {"display": "none"}
497 framesize_style = {"display": "none"}
499 testtype_style = {"display": "none"}
502 if phy and area and test:
503 core_style = {"display": "block"}
505 {"label": v, "value": v}
506 for v in self.spec_tbs[phy][area]["core"]
508 framesize_style = {"display": "block"}
510 {"label": v, "value": v}
511 for v in self.spec_tbs[phy][area]["frame-size"]
513 testtype_style = {"display": "block"}
515 {"label": v, "value": v}
516 for v in self.spec_tbs[phy][area]["test-type"]
521 core_style, core_opts,
522 framesize_style, framesize_opts,
523 testtype_style, testtype_opts,
527 def _sync_checklists(opt, sel, all, id):
530 options = {v["value"] for v in opt}
531 input_id = callback_context.triggered[0]["prop_id"].split(".")[0]
533 all = ["all"] if set(sel) == options else list()
535 sel = list(options) if all else list()
539 Output("cl-ctrl-core", "value"),
540 Output("cl-ctrl-core-all", "value"),
541 State("cl-ctrl-core", "options"),
542 Input("cl-ctrl-core", "value"),
543 Input("cl-ctrl-core-all", "value"),
544 prevent_initial_call=True
546 def _sync_cl_core(opt, sel, all):
547 return _sync_checklists(opt, sel, all, "cl-ctrl-core")
550 Output("cl-ctrl-framesize", "value"),
551 Output("cl-ctrl-framesize-all", "value"),
552 State("cl-ctrl-framesize", "options"),
553 Input("cl-ctrl-framesize", "value"),
554 Input("cl-ctrl-framesize-all", "value"),
555 prevent_initial_call=True
557 def _sync_cl_framesize(opt, sel, all):
558 return _sync_checklists(opt, sel, all, "cl-ctrl-framesize")
561 Output("cl-ctrl-testtype", "value"),
562 Output("cl-ctrl-testtype-all", "value"),
563 State("cl-ctrl-testtype", "options"),
564 Input("cl-ctrl-testtype", "value"),
565 Input("cl-ctrl-testtype-all", "value"),
566 prevent_initial_call=True
568 def _sync_cl_testtype(opt, sel, all):
569 return _sync_checklists(opt, sel, all, "cl-ctrl-testtype")
572 Output("graph-tput", "figure"),
573 Output("graph-latency", "figure"),
574 Output("div-tput", "style"),
575 Output("div-latency", "style"),
576 Output("div-lat-metadata", "style"),
577 Output("div-download", "style"),
578 Output("selected-tests", "data"), # Store
579 Output("cl-selected", "options"), # User selection
580 Output("dd-ctrl-phy", "value"),
581 Output("dd-ctrl-area", "value"),
582 Output("dd-ctrl-test", "value"),
583 State("selected-tests", "data"), # Store
584 State("cl-selected", "value"),
585 State("dd-ctrl-phy", "value"),
586 State("dd-ctrl-area", "value"),
587 State("dd-ctrl-test", "value"),
588 State("cl-ctrl-core", "value"),
589 State("cl-ctrl-framesize", "value"),
590 State("cl-ctrl-testtype", "value"),
591 Input("btn-ctrl-add", "n_clicks"),
592 Input("btn-sel-display", "n_clicks"),
593 Input("btn-sel-remove", "n_clicks"),
594 Input("dpr-period", "start_date"),
595 Input("dpr-period", "end_date"),
596 prevent_initial_call=True
598 def _process_list(store_sel, list_sel, phy, area, test, cores,
599 framesizes, testtypes, btn_add, btn_display, btn_remove,
604 if not (btn_add or btn_display or btn_remove or d_start or d_end):
608 # Display selected tests with checkboxes:
611 {"label": v["id"], "value": v["id"]} for v in store_sel
617 def __init__(self) -> None:
619 "graph-tput-figure": no_update,
620 "graph-lat-figure": no_update,
621 "div-tput-style": no_update,
622 "div-latency-style": no_update,
623 "div-lat-metadata-style": no_update,
624 "div-download-style": no_update,
625 "selected-tests-data": no_update,
626 "cl-selected-options": no_update,
627 "dd-ctrl-phy-value": no_update,
628 "dd-ctrl-area-value": no_update,
629 "dd-ctrl-test-value": no_update,
633 return tuple(self._output.values())
635 def set_values(self, kwargs: dict) -> None:
636 for key, val in kwargs.items():
637 if key in self._output:
638 self._output[key] = val
640 raise KeyError(f"The key {key} is not defined.")
643 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
645 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
647 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
649 output = RetunValue()
651 if trigger_id == "btn-ctrl-add":
652 # Add selected test to the list of tests in store:
653 if phy and area and test and cores and framesizes and testtypes:
654 if store_sel is None:
657 for framesize in framesizes:
658 for ttype in testtypes:
660 f"{phy.replace('af_xdp', 'af-xdp')}-"
662 f"{framesize.lower()}-"
667 if tid not in [itm["id"] for itm in store_sel]:
673 "framesize": framesize.lower(),
674 "core": core.lower(),
675 "testtype": ttype.lower()
678 "selected-tests-data": store_sel,
679 "cl-selected-options": _list_tests(),
680 "dd-ctrl-phy-value": None,
681 "dd-ctrl-area-value": None,
682 "dd-ctrl-test-value": None,
685 elif trigger_id in ("btn-sel-display", "dpr-period"):
686 fig_tput, fig_lat = graph_trending(
687 self.data, store_sel, self.layout, d_start, d_end
690 "graph-tput-figure": \
691 fig_tput if fig_tput else self.NO_GRAPH,
692 "graph-lat-figure": \
693 fig_lat if fig_lat else self.NO_GRAPH,
695 self.STYLE_BLOCK if fig_tput else self.STYLE_HIDEN,
696 "div-latency-style": \
697 self.STYLE_BLOCK if fig_lat else self.STYLE_HIDEN,
698 "div-lat-metadata-style": \
699 self.STYLE_BLOCK if fig_lat else self.STYLE_HIDEN,
700 "div-download-style": \
701 self.STYLE_BLOCK if fig_tput else self.STYLE_HIDEN,
704 elif trigger_id == "btn-sel-remove":
706 new_store_sel = list()
707 for item in store_sel:
708 if item["id"] not in list_sel:
709 new_store_sel.append(item)
710 store_sel = new_store_sel
712 fig_tput, fig_lat = graph_trending(
713 self.data, store_sel, self.layout, d_start, d_end
716 "graph-tput-figure": \
717 fig_tput if fig_tput else self.NO_GRAPH,
718 "graph-lat-figure": \
719 fig_lat if fig_lat else self.NO_GRAPH,
721 self.STYLE_BLOCK if fig_tput else self.STYLE_HIDEN,
722 "div-latency-style": \
723 self.STYLE_BLOCK if fig_lat else self.STYLE_HIDEN,
724 "div-lat-metadata-style": \
725 self.STYLE_BLOCK if fig_lat else self.STYLE_HIDEN,
726 "div-download-style": \
727 self.STYLE_BLOCK if fig_tput else self.STYLE_HIDEN,
728 "selected-tests-data": store_sel,
729 "cl-selected-options": _list_tests()
733 "graph-tput-figure": self.NO_GRAPH,
734 "graph-lat-figure": self.NO_GRAPH,
735 "div-tput-style": self.STYLE_HIDEN,
736 "div-latency-style": self.STYLE_HIDEN,
737 "div-lat-metadata-style": self.STYLE_HIDEN,
738 "div-download-style": self.STYLE_HIDEN,
739 "selected-tests-data": store_sel,
740 "cl-selected-options": _list_tests()
743 return output.value()
746 Output("tput-metadata", "children"),
747 Input("graph-tput", "clickData")
749 def _show_tput_metadata(hover_data):
755 return hover_data["points"][0]["text"].replace("<br>", "\n")
758 Output("graph-latency-hdrh", "figure"),
759 Output("graph-latency-hdrh", "style"),
760 Output("lat-metadata", "children"),
761 Input("graph-latency", "clickData")
763 def _show_latency_hdhr(hover_data):
770 hdrh_data = hover_data["points"][0].get("customdata", None)
772 graph = graph_hdrh_latency(hdrh_data, self.layout)
777 hover_data["points"][0]["text"].replace("<br>", "\n")
781 Output("download-data", "data"),
782 State("selected-tests", "data"),
783 Input("btn-download-data", "n_clicks"),
784 prevent_initial_call=True
786 def _download_data(store_sel, n_clicks):
794 for itm in store_sel:
795 sel_data = select_trending_data(self.data, itm)
798 df = pd.concat([df, sel_data], ignore_index=True)
800 return dcc.send_data_frame(df.to_csv, "trending_data.csv")