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 NO_GRAPH = {"data": [], "layout": {}, "frames": []}
39 def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
46 self._html_layout_file = html_layout_file
47 self._spec_file = spec_file
48 self._graph_layout_file = graph_layout_file
49 self._data_spec_file = data_spec_file
53 data_spec_file=self._data_spec_file,
55 ).read_trending_mrr(days=5)
58 data_spec_file=self._data_spec_file,
60 ).read_trending_ndrpdr(days=14)
62 self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
65 self._html_layout = ""
67 self._graph_layout = None
70 with open(self._html_layout_file, "r") as file_read:
71 self._html_layout = file_read.read()
72 except IOError as err:
74 f"Not possible to open the file {self._html_layout_file}\n{err}"
78 with open(self._spec_file, "r") as file_read:
79 self._spec_tbs = load(file_read, Loader=FullLoader)
80 except IOError as err:
82 f"Not possible to open the file {self._spec_file,}\n{err}"
84 except YAMLError as err:
86 f"An error occurred while parsing the specification file "
87 f"{self._spec_file,}\n"
92 with open(self._graph_layout_file, "r") as file_read:
93 self._graph_layout = load(file_read, Loader=FullLoader)
94 except IOError as err:
96 f"Not possible to open the file {self._graph_layout_file}\n"
99 except YAMLError as err:
101 f"An error occurred while parsing the specification file "
102 f"{self._graph_layout_file}\n"
107 if self._app is not None and hasattr(self, 'callbacks'):
108 self.callbacks(self._app)
111 def html_layout(self):
112 return self._html_layout
116 return self._spec_tbs
124 return self._graph_layout
126 def add_content(self):
129 if self.html_layout and self.spec_tbs:
142 id="offcanvas-metadata",
143 title="Throughput And Latency",
147 dbc.Row(id="metadata-tput-lat"),
148 dbc.Row(id="metadata-hdrh-graph"),
154 class_name="g-0 p-2",
159 self._add_ctrl_col(),
160 self._add_plotting_col(),
178 def _add_navbar(self):
179 """Add nav element with navigation panel. It is placed on the top.
181 return dbc.NavbarSimple(
182 id="navbarsimple-main",
186 "Continuous Performance Trending",
194 brand_external_link=True,
199 def _add_ctrl_col(self) -> dbc.Col:
200 """Add column with controls. It is placed on the left side.
205 self._add_ctrl_panel(),
206 self._add_ctrl_shown()
210 def _add_plotting_col(self) -> dbc.Col:
211 """Add column with plots and tables. It is placed on the right side.
214 id="col-plotting-area",
216 dbc.Row( # Throughput
218 class_name="g-0 p-2",
221 dcc.Graph(id="graph-tput")
227 class_name="g-0 p-2",
230 dcc.Graph(id="graph-latency")
238 dcc.Loading(children=[
240 id="btn-download-data",
241 children=["Download Data"]
243 dcc.Download(id="download-data")
251 def _add_ctrl_panel(self) -> dbc.Row:
258 dbc.Label("Physical Test Bed Topology, NIC and Driver"),
261 placeholder="Select a Physical Test Bed Topology...",
265 {"label": k, "value": k} for k in self.spec_tbs.keys()
271 placeholder="Select an Area...",
279 placeholder="Select a Test...",
288 dbc.Label("Number of Cores"),
291 id="cl-ctrl-core-all",
292 options=[{"label": "All", "value": "all"}, ],
307 id="row-ctrl-framesize",
310 dbc.Label("Frame Size"),
313 id="cl-ctrl-framesize-all",
314 options=[{"label": "All", "value": "all"}, ],
321 id="cl-ctrl-framesize",
329 id="row-ctrl-testtype",
332 dbc.Label("Test Type"),
335 id="cl-ctrl-testtype-all",
336 options=[{"label": "All", "value": "all"}, ],
343 id="cl-ctrl-testtype",
365 datetime.utcnow()-timedelta(days=180),
366 max_date_allowed=datetime.utcnow(),
367 initial_visible_month=datetime.utcnow(),
368 start_date=datetime.utcnow() - timedelta(days=180),
369 end_date=datetime.utcnow(),
370 display_format="D MMMM YY"
377 def _add_ctrl_shown(self) -> dbc.Row:
387 dbc.Label("Selected tests"),
402 children="Remove Selected",
407 id="btn-sel-remove-all",
408 children="Remove All",
413 id="btn-sel-display",
427 def callbacks(self, app):
430 Output("dd-ctrl-area", "options"),
431 Output("dd-ctrl-area", "disabled"),
432 Input("dd-ctrl-phy", "value"),
434 def _update_dd_area(phy):
443 {"label": self.spec_tbs[phy][v]["label"], "value": v}
444 for v in [v for v in self.spec_tbs[phy].keys()]
451 return options, disable
454 Output("dd-ctrl-test", "options"),
455 Output("dd-ctrl-test", "disabled"),
456 State("dd-ctrl-phy", "value"),
457 Input("dd-ctrl-area", "value"),
459 def _update_dd_test(phy, area):
468 {"label": v, "value": v}
469 for v in self.spec_tbs[phy][area]["test"]
476 return options, disable
479 Output("cl-ctrl-core", "options"),
480 Output("cl-ctrl-framesize", "options"),
481 Output("cl-ctrl-testtype", "options"),
482 State("dd-ctrl-phy", "value"),
483 State("dd-ctrl-area", "value"),
484 Input("dd-ctrl-test", "value"),
486 def _update_btn_add(phy, area, test):
496 if phy and area and test:
498 {"label": v, "value": v}
499 for v in self.spec_tbs[phy][area]["core"]
502 {"label": v, "value": v}
503 for v in self.spec_tbs[phy][area]["frame-size"]
506 {"label": v, "value": v}
507 for v in self.spec_tbs[phy][area]["test-type"]
516 def _sync_checklists(opt, sel, all, id):
519 options = {v["value"] for v in opt}
520 input_id = callback_context.triggered[0]["prop_id"].split(".")[0]
522 all = ["all"] if set(sel) == options else list()
524 sel = list(options) if all else list()
528 Output("cl-ctrl-core", "value"),
529 Output("cl-ctrl-core-all", "value"),
530 State("cl-ctrl-core", "options"),
531 Input("cl-ctrl-core", "value"),
532 Input("cl-ctrl-core-all", "value"),
533 prevent_initial_call=True
535 def _sync_cl_core(opt, sel, all):
536 return _sync_checklists(opt, sel, all, "cl-ctrl-core")
539 Output("cl-ctrl-framesize", "value"),
540 Output("cl-ctrl-framesize-all", "value"),
541 State("cl-ctrl-framesize", "options"),
542 Input("cl-ctrl-framesize", "value"),
543 Input("cl-ctrl-framesize-all", "value"),
544 prevent_initial_call=True
546 def _sync_cl_framesize(opt, sel, all):
547 return _sync_checklists(opt, sel, all, "cl-ctrl-framesize")
550 Output("cl-ctrl-testtype", "value"),
551 Output("cl-ctrl-testtype-all", "value"),
552 State("cl-ctrl-testtype", "options"),
553 Input("cl-ctrl-testtype", "value"),
554 Input("cl-ctrl-testtype-all", "value"),
555 prevent_initial_call=True
557 def _sync_cl_testtype(opt, sel, all):
558 return _sync_checklists(opt, sel, all, "cl-ctrl-testtype")
561 Output("graph-tput", "figure"),
562 Output("graph-latency", "figure"),
563 Output("selected-tests", "data"), # Store
564 Output("cl-selected", "options"), # User selection
565 Output("dd-ctrl-phy", "value"),
566 Output("dd-ctrl-area", "value"),
567 Output("dd-ctrl-test", "value"),
568 State("selected-tests", "data"), # Store
569 State("cl-selected", "value"),
570 State("dd-ctrl-phy", "value"),
571 State("dd-ctrl-area", "value"),
572 State("dd-ctrl-test", "value"),
573 State("cl-ctrl-core", "value"),
574 State("cl-ctrl-framesize", "value"),
575 State("cl-ctrl-testtype", "value"),
576 Input("btn-ctrl-add", "n_clicks"),
577 Input("btn-sel-display", "n_clicks"),
578 Input("btn-sel-remove", "n_clicks"),
579 Input("btn-sel-remove-all", "n_clicks"),
580 Input("dpr-period", "start_date"),
581 Input("dpr-period", "end_date"),
582 prevent_initial_call=True
584 def _process_list(store_sel, list_sel, phy, area, test, cores,
585 framesizes, testtypes, btn_add, btn_display, btn_remove,
586 btn_remove_all, d_start, d_end):
590 if not (btn_add or btn_display or btn_remove or btn_remove_all or \
595 # Display selected tests with checkboxes:
598 {"label": v["id"], "value": v["id"]} for v in store_sel
604 def __init__(self) -> None:
606 "graph-tput-figure": no_update,
607 "graph-lat-figure": no_update,
608 "selected-tests-data": no_update,
609 "cl-selected-options": no_update,
610 "dd-ctrl-phy-value": no_update,
611 "dd-ctrl-area-value": no_update,
612 "dd-ctrl-test-value": no_update,
616 return tuple(self._output.values())
618 def set_values(self, kwargs: dict) -> None:
619 for key, val in kwargs.items():
620 if key in self._output:
621 self._output[key] = val
623 raise KeyError(f"The key {key} is not defined.")
626 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
628 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
630 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
632 output = RetunValue()
634 if trigger_id == "btn-ctrl-add":
635 # Add selected test to the list of tests in store:
636 if phy and area and test and cores and framesizes and testtypes:
637 if store_sel is None:
640 for framesize in framesizes:
641 for ttype in testtypes:
643 f"{phy.replace('af_xdp', 'af-xdp')}-"
645 f"{framesize.lower()}-"
650 if tid not in [itm["id"] for itm in store_sel]:
656 "framesize": framesize.lower(),
657 "core": core.lower(),
658 "testtype": ttype.lower()
661 "selected-tests-data": store_sel,
662 "cl-selected-options": _list_tests(),
663 "dd-ctrl-phy-value": None,
664 "dd-ctrl-area-value": None,
665 "dd-ctrl-test-value": None,
668 elif trigger_id in ("btn-sel-display", "dpr-period"):
669 fig_tput, fig_lat = graph_trending(
670 self.data, store_sel, self.layout, d_start, d_end
673 "graph-tput-figure": \
674 fig_tput if fig_tput else self.NO_GRAPH,
675 "graph-lat-figure": \
676 fig_lat if fig_lat else self.NO_GRAPH,
678 elif trigger_id == "btn-sel-remove-all":
680 "graph-tput-figure": self.NO_GRAPH,
681 "graph-lat-figure": self.NO_GRAPH,
682 "selected-tests-data": list(),
683 "cl-selected-options": list()
685 elif trigger_id == "btn-sel-remove":
687 new_store_sel = list()
688 for item in store_sel:
689 if item["id"] not in list_sel:
690 new_store_sel.append(item)
691 store_sel = new_store_sel
693 fig_tput, fig_lat = graph_trending(
694 self.data, store_sel, self.layout, d_start, d_end
697 "graph-tput-figure": \
698 fig_tput if fig_tput else self.NO_GRAPH,
699 "graph-lat-figure": \
700 fig_lat if fig_lat else self.NO_GRAPH,
701 "selected-tests-data": store_sel,
702 "cl-selected-options": _list_tests()
706 "graph-tput-figure": self.NO_GRAPH,
707 "graph-lat-figure": self.NO_GRAPH,
708 "selected-tests-data": store_sel,
709 "cl-selected-options": _list_tests()
712 return output.value()
715 Output("metadata-tput-lat", "children"),
716 Output("metadata-hdrh-graph", "children"),
717 Output("offcanvas-metadata", "is_open"),
718 Input("graph-tput", "clickData"),
719 Input("graph-latency", "clickData")
721 def _show_tput_metadata(tput_data, lat_data) -> dbc.Card:
724 if not (tput_data or lat_data):
730 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
731 if trigger_id == "graph-tput":
733 txt = tput_data["points"][0]["text"].replace("<br>", "\n")
734 elif trigger_id == "graph-latency":
736 txt = lat_data["points"][0]["text"].replace("<br>", "\n")
737 hdrh_data = lat_data["points"][0].get("customdata", None)
740 id="hdrh-latency-graph",
741 figure=graph_hdrh_latency(hdrh_data, self.layout)
747 dbc.CardHeader(children=[
749 target_id="tput-lat-metadata",
751 style={"display": "inline-block"}
756 id="tput-lat-metadata",
763 return metadata, graph, True
766 Output("download-data", "data"),
767 State("selected-tests", "data"),
768 Input("btn-download-data", "n_clicks"),
769 prevent_initial_call=True
771 def _download_data(store_sel, n_clicks):
779 for itm in store_sel:
780 sel_data = select_trending_data(self.data, itm)
783 df = pd.concat([df, sel_data], ignore_index=True)
785 return dcc.send_data_frame(df.to_csv, "trending_data.csv")