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",
195 brand_external_link=True,
200 def _add_ctrl_col(self) -> dbc.Col:
201 """Add column with controls. It is placed on the left side.
206 self._add_ctrl_panel(),
207 self._add_ctrl_shown()
211 def _add_plotting_col(self) -> dbc.Col:
212 """Add column with plots and tables. It is placed on the right side.
215 id="col-plotting-area",
217 dbc.Row( # Throughput
219 class_name="g-0 p-2",
222 dcc.Graph(id="graph-tput")
228 class_name="g-0 p-2",
231 dcc.Graph(id="graph-latency")
239 dcc.Loading(children=[
241 id="btn-download-data",
242 children=["Download Data"]
244 dcc.Download(id="download-data")
252 def _add_ctrl_panel(self) -> dbc.Row:
259 dbc.Label("Physical Test Bed Topology, NIC and Driver"),
263 placeholder="Select a Physical Test Bed Topology...",
265 {"label": k, "value": k} for k in self.spec_tbs.keys()
273 placeholder="Select an Area...",
281 placeholder="Select a Test...",
289 dbc.Label("Number of Cores"),
292 id="cl-ctrl-core-all",
293 options=[{"label": "All", "value": "all"}, ],
308 id="row-ctrl-framesize",
311 dbc.Label("Frame Size"),
314 id="cl-ctrl-framesize-all",
315 options=[{"label": "All", "value": "all"}, ],
322 id="cl-ctrl-framesize",
330 id="row-ctrl-testtype",
333 dbc.Label("Test Type"),
336 id="cl-ctrl-testtype-all",
337 options=[{"label": "All", "value": "all"}, ],
344 id="cl-ctrl-testtype",
366 datetime.utcnow()-timedelta(days=180),
367 max_date_allowed=datetime.utcnow(),
368 initial_visible_month=datetime.utcnow(),
369 start_date=datetime.utcnow() - timedelta(days=180),
370 end_date=datetime.utcnow(),
371 display_format="D MMMM YY"
378 def _add_ctrl_shown(self) -> dbc.Row:
388 dbc.Label("Selected tests"),
402 id="btn-sel-remove-all",
403 children="Remove All",
409 children="Remove Selected",
414 id="btn-sel-display",
428 def callbacks(self, app):
431 Output("dd-ctrl-area", "options"),
432 Output("dd-ctrl-area", "disabled"),
433 Input("dd-ctrl-phy", "value"),
435 def _update_dd_area(phy):
444 {"label": self.spec_tbs[phy][v]["label"], "value": v}
445 for v in [v for v in self.spec_tbs[phy].keys()]
452 return options, disable
455 Output("dd-ctrl-test", "options"),
456 Output("dd-ctrl-test", "disabled"),
457 State("dd-ctrl-phy", "value"),
458 Input("dd-ctrl-area", "value"),
460 def _update_dd_test(phy, area):
469 {"label": v, "value": v}
470 for v in self.spec_tbs[phy][area]["test"]
477 return options, disable
480 Output("cl-ctrl-core", "options"),
481 Output("cl-ctrl-framesize", "options"),
482 Output("cl-ctrl-testtype", "options"),
483 State("dd-ctrl-phy", "value"),
484 State("dd-ctrl-area", "value"),
485 Input("dd-ctrl-test", "value"),
487 def _update_btn_add(phy, area, test):
497 if phy and area and test:
499 {"label": v, "value": v}
500 for v in self.spec_tbs[phy][area]["core"]
503 {"label": v, "value": v}
504 for v in self.spec_tbs[phy][area]["frame-size"]
507 {"label": v, "value": v}
508 for v in self.spec_tbs[phy][area]["test-type"]
517 def _sync_checklists(opt, sel, all, id):
520 options = {v["value"] for v in opt}
521 input_id = callback_context.triggered[0]["prop_id"].split(".")[0]
523 all = ["all"] if set(sel) == options else list()
525 sel = list(options) if all else list()
529 Output("cl-ctrl-core", "value"),
530 Output("cl-ctrl-core-all", "value"),
531 State("cl-ctrl-core", "options"),
532 Input("cl-ctrl-core", "value"),
533 Input("cl-ctrl-core-all", "value"),
534 prevent_initial_call=True
536 def _sync_cl_core(opt, sel, all):
537 return _sync_checklists(opt, sel, all, "cl-ctrl-core")
540 Output("cl-ctrl-framesize", "value"),
541 Output("cl-ctrl-framesize-all", "value"),
542 State("cl-ctrl-framesize", "options"),
543 Input("cl-ctrl-framesize", "value"),
544 Input("cl-ctrl-framesize-all", "value"),
545 prevent_initial_call=True
547 def _sync_cl_framesize(opt, sel, all):
548 return _sync_checklists(opt, sel, all, "cl-ctrl-framesize")
551 Output("cl-ctrl-testtype", "value"),
552 Output("cl-ctrl-testtype-all", "value"),
553 State("cl-ctrl-testtype", "options"),
554 Input("cl-ctrl-testtype", "value"),
555 Input("cl-ctrl-testtype-all", "value"),
556 prevent_initial_call=True
558 def _sync_cl_testtype(opt, sel, all):
559 return _sync_checklists(opt, sel, all, "cl-ctrl-testtype")
562 Output("graph-tput", "figure"),
563 Output("graph-latency", "figure"),
564 Output("selected-tests", "data"), # Store
565 Output("cl-selected", "options"), # User selection
566 Output("dd-ctrl-phy", "value"),
567 Output("dd-ctrl-area", "value"),
568 Output("dd-ctrl-test", "value"),
569 State("selected-tests", "data"), # Store
570 State("cl-selected", "value"),
571 State("dd-ctrl-phy", "value"),
572 State("dd-ctrl-area", "value"),
573 State("dd-ctrl-test", "value"),
574 State("cl-ctrl-core", "value"),
575 State("cl-ctrl-framesize", "value"),
576 State("cl-ctrl-testtype", "value"),
577 Input("btn-ctrl-add", "n_clicks"),
578 Input("btn-sel-display", "n_clicks"),
579 Input("btn-sel-remove", "n_clicks"),
580 Input("btn-sel-remove-all", "n_clicks"),
581 Input("dpr-period", "start_date"),
582 Input("dpr-period", "end_date"),
583 prevent_initial_call=True
585 def _process_list(store_sel, list_sel, phy, area, test, cores,
586 framesizes, testtypes, btn_add, btn_display, btn_remove,
587 btn_remove_all, d_start, d_end):
591 if not (btn_add or btn_display or btn_remove or btn_remove_all or \
596 # Display selected tests with checkboxes:
599 {"label": v["id"], "value": v["id"]} for v in store_sel
605 def __init__(self) -> None:
607 "graph-tput-figure": no_update,
608 "graph-lat-figure": no_update,
609 "selected-tests-data": no_update,
610 "cl-selected-options": no_update,
611 "dd-ctrl-phy-value": no_update,
612 "dd-ctrl-area-value": no_update,
613 "dd-ctrl-test-value": no_update,
617 return tuple(self._output.values())
619 def set_values(self, kwargs: dict) -> None:
620 for key, val in kwargs.items():
621 if key in self._output:
622 self._output[key] = val
624 raise KeyError(f"The key {key} is not defined.")
627 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
629 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
631 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
633 output = RetunValue()
635 if trigger_id == "btn-ctrl-add":
636 # Add selected test to the list of tests in store:
637 if phy and area and test and cores and framesizes and testtypes:
638 if store_sel is None:
641 for framesize in framesizes:
642 for ttype in testtypes:
644 f"{phy.replace('af_xdp', 'af-xdp')}-"
646 f"{framesize.lower()}-"
651 if tid not in [itm["id"] for itm in store_sel]:
657 "framesize": framesize.lower(),
658 "core": core.lower(),
659 "testtype": ttype.lower()
662 "selected-tests-data": store_sel,
663 "cl-selected-options": _list_tests(),
664 "dd-ctrl-phy-value": None,
665 "dd-ctrl-area-value": None,
666 "dd-ctrl-test-value": None,
669 elif trigger_id in ("btn-sel-display", "dpr-period"):
670 fig_tput, fig_lat = graph_trending(
671 self.data, store_sel, self.layout, d_start, d_end
674 "graph-tput-figure": \
675 fig_tput if fig_tput else self.NO_GRAPH,
676 "graph-lat-figure": \
677 fig_lat if fig_lat else self.NO_GRAPH,
679 elif trigger_id == "btn-sel-remove-all":
681 "graph-tput-figure": self.NO_GRAPH,
682 "graph-lat-figure": self.NO_GRAPH,
683 "selected-tests-data": list(),
684 "cl-selected-options": list()
686 elif trigger_id == "btn-sel-remove":
688 new_store_sel = list()
689 for item in store_sel:
690 if item["id"] not in list_sel:
691 new_store_sel.append(item)
692 store_sel = new_store_sel
694 fig_tput, fig_lat = graph_trending(
695 self.data, store_sel, self.layout, d_start, d_end
698 "graph-tput-figure": \
699 fig_tput if fig_tput else self.NO_GRAPH,
700 "graph-lat-figure": \
701 fig_lat if fig_lat else self.NO_GRAPH,
702 "selected-tests-data": store_sel,
703 "cl-selected-options": _list_tests()
707 "graph-tput-figure": self.NO_GRAPH,
708 "graph-lat-figure": self.NO_GRAPH,
709 "selected-tests-data": store_sel,
710 "cl-selected-options": _list_tests()
713 return output.value()
716 Output("metadata-tput-lat", "children"),
717 Output("metadata-hdrh-graph", "children"),
718 Output("offcanvas-metadata", "is_open"),
719 Input("graph-tput", "clickData"),
720 Input("graph-latency", "clickData")
722 def _show_tput_metadata(tput_data, lat_data) -> dbc.Card:
725 if not (tput_data or lat_data):
731 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
732 if trigger_id == "graph-tput":
734 txt = tput_data["points"][0]["text"].replace("<br>", "\n")
735 elif trigger_id == "graph-latency":
737 txt = lat_data["points"][0]["text"].replace("<br>", "\n")
738 hdrh_data = lat_data["points"][0].get("customdata", None)
741 id="hdrh-latency-graph",
742 figure=graph_hdrh_latency(hdrh_data, self.layout)
748 dbc.CardHeader(children=[
750 target_id="tput-lat-metadata",
752 style={"display": "inline-block"}
757 id="tput-lat-metadata",
764 return metadata, graph, True
767 Output("download-data", "data"),
768 State("selected-tests", "data"),
769 Input("btn-download-data", "n_clicks"),
770 prevent_initial_call=True
772 def _download_data(store_sel, n_clicks):
780 for itm in store_sel:
781 sel_data = select_trending_data(self.data, itm)
784 df = pd.concat([df, sel_data], ignore_index=True)
786 return dcc.send_data_frame(df.to_csv, "trending_data.csv")