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.
18 import dash_bootstrap_components as dbc
22 from dash import callback_context, no_update
23 from dash import Input, Output, State
24 from dash.exceptions import PreventUpdate
25 from yaml import load, FullLoader, YAMLError
26 from datetime import datetime, timedelta
27 from copy import deepcopy
29 from ..data.data import Data
30 from .graphs import graph_trending, graph_hdrh_latency, \
38 STYLE_DISABLED = {"display": "none"}
39 STYLE_ENABLED = {"display": "inherit"}
52 PLACEHOLDER = html.Nobr("")
54 def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
61 self._html_layout_file = html_layout_file
62 self._spec_file = spec_file
63 self._graph_layout_file = graph_layout_file
64 self._data_spec_file = data_spec_file
68 data_spec_file=self._data_spec_file,
70 ).read_trending_mrr(days=5)
73 data_spec_file=self._data_spec_file,
75 ).read_trending_ndrpdr(days=14)
77 self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
80 self._html_layout = ""
82 self._graph_layout = None
85 with open(self._html_layout_file, "r") as file_read:
86 self._html_layout = file_read.read()
87 except IOError as err:
89 f"Not possible to open the file {self._html_layout_file}\n{err}"
93 with open(self._spec_file, "r") as file_read:
94 self._spec_tbs = load(file_read, Loader=FullLoader)
95 except IOError as err:
97 f"Not possible to open the file {self._spec_file,}\n{err}"
99 except YAMLError as err:
101 f"An error occurred while parsing the specification file "
102 f"{self._spec_file,}\n"
107 with open(self._graph_layout_file, "r") as file_read:
108 self._graph_layout = load(file_read, Loader=FullLoader)
109 except IOError as err:
111 f"Not possible to open the file {self._graph_layout_file}\n"
114 except YAMLError as err:
116 f"An error occurred while parsing the specification file "
117 f"{self._graph_layout_file}\n"
122 if self._app is not None and hasattr(self, 'callbacks'):
123 self.callbacks(self._app)
126 def html_layout(self):
127 return self._html_layout
131 return self._spec_tbs
139 return self._graph_layout
141 def add_content(self):
144 if self.html_layout and self.spec_tbs:
158 id="offcanvas-metadata",
159 title="Throughput And Latency",
163 dbc.Row(id="metadata-tput-lat"),
164 dbc.Row(id="metadata-hdrh-graph"),
178 self._add_ctrl_col(),
179 self._add_plotting_col(),
197 def _add_navbar(self):
198 """Add nav element with navigation panel. It is placed on the top.
200 return dbc.NavbarSimple(
201 id="navbarsimple-main",
205 "Continuous Performance Trending",
214 brand_external_link=True,
219 def _add_ctrl_col(self) -> dbc.Col:
220 """Add column with controls. It is placed on the left side.
225 self._add_ctrl_panel(),
229 def _add_plotting_col(self) -> dbc.Col:
230 """Add column with plots and tables. It is placed on the right side.
233 id="col-plotting-area",
235 dbc.Row( # Throughput
237 class_name="g-0 p-2",
244 class_name="g-0 p-2",
250 id="row-btn-download",
251 class_name="g-0 p-2",
260 def _add_ctrl_panel(self) -> dbc.Row:
265 class_name="g-0 p-2",
271 "Physical Test Bed Topology, NIC and Driver",
276 placeholder="Select a Physical Test Bed Topology...",
278 {"label": k, "value": k} for k in self.spec_tbs.keys()
293 placeholder="Select an Area...",
308 placeholder="Select a Test...",
325 id="cl-ctrl-core-all",
326 options=self.CL_ALL_DISABLED,
345 id="row-ctrl-framesize",
355 id="cl-ctrl-framesize-all",
356 options=self.CL_ALL_DISABLED,
366 id="cl-ctrl-framesize",
375 id="row-ctrl-testtype",
385 id="cl-ctrl-testtype-all",
386 options=self.CL_ALL_DISABLED,
396 id="cl-ctrl-testtype",
411 children="Add Selected",
424 className="d-flex justify-content-center",
426 datetime.utcnow()-timedelta(days=180),
427 max_date_allowed=datetime.utcnow(),
428 initial_visible_month=datetime.utcnow(),
429 start_date=datetime.utcnow() - timedelta(days=180),
430 end_date=datetime.utcnow(),
431 display_format="D MMMM YY"
436 id="row-card-sel-tests",
438 style=self.STYLE_DISABLED,
445 class_name="overflow-auto",
449 style={"max-height": "12em"},
454 id="row-btns-sel-tests",
455 style=self.STYLE_DISABLED,
460 id="btn-sel-remove-all",
461 children="Remove All",
468 children="Remove Selected",
482 def __init__(self, panel: dict) -> None:
490 # Defines also the order of keys
492 "dd-ctrl-phy-value": str(),
493 "dd-ctrl-area-options": list(),
494 "dd-ctrl-area-disabled": True,
495 "dd-ctrl-area-value": str(),
496 "dd-ctrl-test-options": list(),
497 "dd-ctrl-test-disabled": True,
498 "dd-ctrl-test-value": str(),
499 "cl-ctrl-core-options": list(),
500 "cl-ctrl-core-value": list(),
501 "cl-ctrl-core-all-value": list(),
502 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
503 "cl-ctrl-framesize-options": list(),
504 "cl-ctrl-framesize-value": list(),
505 "cl-ctrl-framesize-all-value": list(),
506 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
507 "cl-ctrl-testtype-options": list(),
508 "cl-ctrl-testtype-value": list(),
509 "cl-ctrl-testtype-all-value": list(),
510 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
511 "btn-ctrl-add-disabled": True,
512 "cl-selected-options": list(),
515 self._panel = deepcopy(self._defaults)
517 for key in self._defaults:
518 self._panel[key] = panel[key]
521 def defaults(self) -> dict:
522 return self._defaults
525 def panel(self) -> dict:
528 def set(self, kwargs: dict) -> None:
529 for key, val in kwargs.items():
530 if key in self._panel:
531 self._panel[key] = val
533 raise KeyError(f"The key {key} is not defined.")
535 def get(self, key: str) -> any:
536 return self._panel[key]
538 def values(self) -> tuple:
539 return tuple(self._panel.values())
542 def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
545 options = {v["value"] for v in opt}
547 sel = list(options) if all else list()
549 all = ["all", ] if set(sel) == options else list()
553 def _list_tests(selection: dict) -> list:
554 """Display selected tests with checkboxes
558 {"label": v["id"], "value": v["id"]} for v in selection
563 def callbacks(self, app):
565 def _generate_plotting_arrea(args: tuple) -> tuple:
569 (fig_tput, fig_lat) = args
571 row_fig_tput = self.PLACEHOLDER
572 row_fig_lat = self.PLACEHOLDER
573 row_btn_dwnld = self.PLACEHOLDER
585 dcc.Loading(children=[
587 id="btn-download-data",
588 children=["Download Data"]
590 dcc.Download(id="download-data")
603 return row_fig_tput, row_fig_lat, row_btn_dwnld
606 Output("control-panel", "data"), # Store
607 Output("selected-tests", "data"), # Store
608 Output("row-graph-tput", "children"),
609 Output("row-graph-lat", "children"),
610 Output("row-btn-download", "children"),
611 Output("row-card-sel-tests", "style"),
612 Output("row-btns-sel-tests", "style"),
613 Output("dd-ctrl-phy", "value"),
614 Output("dd-ctrl-area", "options"),
615 Output("dd-ctrl-area", "disabled"),
616 Output("dd-ctrl-area", "value"),
617 Output("dd-ctrl-test", "options"),
618 Output("dd-ctrl-test", "disabled"),
619 Output("dd-ctrl-test", "value"),
620 Output("cl-ctrl-core", "options"),
621 Output("cl-ctrl-core", "value"),
622 Output("cl-ctrl-core-all", "value"),
623 Output("cl-ctrl-core-all", "options"),
624 Output("cl-ctrl-framesize", "options"),
625 Output("cl-ctrl-framesize", "value"),
626 Output("cl-ctrl-framesize-all", "value"),
627 Output("cl-ctrl-framesize-all", "options"),
628 Output("cl-ctrl-testtype", "options"),
629 Output("cl-ctrl-testtype", "value"),
630 Output("cl-ctrl-testtype-all", "value"),
631 Output("cl-ctrl-testtype-all", "options"),
632 Output("btn-ctrl-add", "disabled"),
633 Output("cl-selected", "options"), # User selection
634 State("control-panel", "data"), # Store
635 State("selected-tests", "data"), # Store
636 State("cl-selected", "value"), # User selection
637 Input("dd-ctrl-phy", "value"),
638 Input("dd-ctrl-area", "value"),
639 Input("dd-ctrl-test", "value"),
640 Input("cl-ctrl-core", "value"),
641 Input("cl-ctrl-core-all", "value"),
642 Input("cl-ctrl-framesize", "value"),
643 Input("cl-ctrl-framesize-all", "value"),
644 Input("cl-ctrl-testtype", "value"),
645 Input("cl-ctrl-testtype-all", "value"),
646 Input("btn-ctrl-add", "n_clicks"),
647 Input("dpr-period", "start_date"),
648 Input("dpr-period", "end_date"),
649 Input("btn-sel-remove", "n_clicks"),
650 Input("btn-sel-remove-all", "n_clicks"),
652 def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
653 dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
654 cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
655 cl_testtype: list, cl_testtype_all: list, btn_add: int,
656 d_start: str, d_end: str, btn_remove: int,
657 btn_remove_all: int) -> tuple:
661 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
663 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
665 row_fig_tput = no_update
666 row_fig_lat = no_update
667 row_btn_dwnld = no_update
668 row_card_sel_tests = no_update
669 row_btns_sel_tests = no_update
671 ctrl_panel = self.ControlPanel(cp_data)
673 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
675 if trigger_id == "dd-ctrl-phy":
678 {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
679 for v in [v for v in self.spec_tbs[dd_phy].keys()]
686 "dd-ctrl-phy-value": dd_phy,
687 "dd-ctrl-area-value": str(),
688 "dd-ctrl-area-options": options,
689 "dd-ctrl-area-disabled": disabled,
690 "dd-ctrl-test-options": list(),
691 "dd-ctrl-test-disabled": True,
692 "cl-ctrl-core-options": list(),
693 "cl-ctrl-core-value": list(),
694 "cl-ctrl-core-all-value": list(),
695 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
696 "cl-ctrl-framesize-options": list(),
697 "cl-ctrl-framesize-value": list(),
698 "cl-ctrl-framesize-all-value": list(),
699 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
700 "cl-ctrl-testtype-options": list(),
701 "cl-ctrl-testtype-value": list(),
702 "cl-ctrl-testtype-all-value": list(),
703 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
704 "btn-ctrl-add-disabled": True,
706 elif trigger_id == "dd-ctrl-area":
708 phy = ctrl_panel.get("dd-ctrl-phy-value")
710 {"label": v, "value": v}
711 for v in self.spec_tbs[phy][dd_area]["test"]
718 "dd-ctrl-area-value": dd_area,
719 "dd-ctrl-test-value": str(),
720 "dd-ctrl-test-options": options,
721 "dd-ctrl-test-disabled": disabled,
722 "cl-ctrl-core-options": list(),
723 "cl-ctrl-core-value": list(),
724 "cl-ctrl-core-all-value": list(),
725 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
726 "cl-ctrl-framesize-options": list(),
727 "cl-ctrl-framesize-value": list(),
728 "cl-ctrl-framesize-all-value": list(),
729 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
730 "cl-ctrl-testtype-options": list(),
731 "cl-ctrl-testtype-value": list(),
732 "cl-ctrl-testtype-all-value": list(),
733 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
734 "btn-ctrl-add-disabled": True,
736 elif trigger_id == "dd-ctrl-test":
738 framesize_opts = list()
739 testtype_opts = list()
740 phy = ctrl_panel.get("dd-ctrl-phy-value")
741 area = ctrl_panel.get("dd-ctrl-area-value")
742 if phy and area and dd_test:
744 {"label": v, "value": v}
745 for v in self.spec_tbs[phy][area]["core"]
748 {"label": v, "value": v}
749 for v in self.spec_tbs[phy][area]["frame-size"]
752 {"label": v, "value": v}
753 for v in self.spec_tbs[phy][area]["test-type"]
756 "dd-ctrl-test-value": dd_test,
757 "cl-ctrl-core-options": core_opts,
758 "cl-ctrl-core-value": list(),
759 "cl-ctrl-core-all-value": list(),
760 "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
761 "cl-ctrl-framesize-options": framesize_opts,
762 "cl-ctrl-framesize-value": list(),
763 "cl-ctrl-framesize-all-value": list(),
764 "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
765 "cl-ctrl-testtype-options": testtype_opts,
766 "cl-ctrl-testtype-value": list(),
767 "cl-ctrl-testtype-all-value": list(),
768 "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
769 "btn-ctrl-add-disabled": False,
771 elif trigger_id == "cl-ctrl-core":
772 val_sel, val_all = self._sync_checklists(
773 opt=ctrl_panel.get("cl-ctrl-core-options"),
779 "cl-ctrl-core-value": val_sel,
780 "cl-ctrl-core-all-value": val_all,
782 elif trigger_id == "cl-ctrl-core-all":
783 val_sel, val_all = self._sync_checklists(
784 opt = ctrl_panel.get("cl-ctrl-core-options"),
790 "cl-ctrl-core-value": val_sel,
791 "cl-ctrl-core-all-value": val_all,
793 elif trigger_id == "cl-ctrl-framesize":
794 val_sel, val_all = self._sync_checklists(
795 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
801 "cl-ctrl-framesize-value": val_sel,
802 "cl-ctrl-framesize-all-value": val_all,
804 elif trigger_id == "cl-ctrl-framesize-all":
805 val_sel, val_all = self._sync_checklists(
806 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
808 all=cl_framesize_all,
812 "cl-ctrl-framesize-value": val_sel,
813 "cl-ctrl-framesize-all-value": val_all,
815 elif trigger_id == "cl-ctrl-testtype":
816 val_sel, val_all = self._sync_checklists(
817 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
823 "cl-ctrl-testtype-value": val_sel,
824 "cl-ctrl-testtype-all-value": val_all,
826 elif trigger_id == "cl-ctrl-testtype-all":
827 val_sel, val_all = self._sync_checklists(
828 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
834 "cl-ctrl-testtype-value": val_sel,
835 "cl-ctrl-testtype-all-value": val_all,
837 elif trigger_id == "btn-ctrl-add":
839 phy = ctrl_panel.get("dd-ctrl-phy-value")
840 area = ctrl_panel.get("dd-ctrl-area-value")
841 test = ctrl_panel.get("dd-ctrl-test-value")
842 cores = ctrl_panel.get("cl-ctrl-core-value")
843 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
844 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
845 # Add selected test to the list of tests in store:
846 if phy and area and test and cores and framesizes and testtypes:
847 if store_sel is None:
850 for framesize in framesizes:
851 for ttype in testtypes:
853 f"{phy.replace('af_xdp', 'af-xdp')}-"
855 f"{framesize.lower()}-"
860 if tid not in [itm["id"] for itm in store_sel]:
866 "framesize": framesize.lower(),
867 "core": core.lower(),
868 "testtype": ttype.lower()
870 row_card_sel_tests = self.STYLE_ENABLED
871 row_btns_sel_tests = self.STYLE_ENABLED
872 ctrl_panel.set(ctrl_panel.defaults)
874 "cl-selected-options": self._list_tests(store_sel)
876 row_fig_tput, row_fig_lat, row_btn_dwnld = \
877 _generate_plotting_arrea(
879 self.data, store_sel, self.layout, d_start, d_end
882 elif trigger_id == "dpr-period":
883 row_fig_tput, row_fig_lat, row_btn_dwnld = \
884 _generate_plotting_arrea(
886 self.data, store_sel, self.layout, d_start, d_end
889 elif trigger_id == "btn-sel-remove-all":
891 row_fig_tput = self.PLACEHOLDER
892 row_fig_lat = self.PLACEHOLDER
893 row_btn_dwnld = self.PLACEHOLDER
894 row_card_sel_tests = self.STYLE_DISABLED
895 row_btns_sel_tests = self.STYLE_DISABLED
898 "cl-selected-options": list()
900 elif trigger_id == "btn-sel-remove":
903 new_store_sel = list()
904 for item in store_sel:
905 if item["id"] not in list_sel:
906 new_store_sel.append(item)
907 store_sel = new_store_sel
909 row_fig_tput, row_fig_lat, row_btn_dwnld = \
910 _generate_plotting_arrea(
912 self.data, store_sel, self.layout, d_start, d_end
916 "cl-selected-options": self._list_tests(store_sel)
919 row_fig_tput = self.PLACEHOLDER
920 row_fig_lat = self.PLACEHOLDER
921 row_btn_dwnld = self.PLACEHOLDER
922 row_card_sel_tests = self.STYLE_DISABLED
923 row_btns_sel_tests = self.STYLE_DISABLED
926 "cl-selected-options": list()
930 ctrl_panel.panel, store_sel,
931 row_fig_tput, row_fig_lat, row_btn_dwnld,
932 row_card_sel_tests, row_btns_sel_tests
934 ret_val.extend(ctrl_panel.values())
938 Output("metadata-tput-lat", "children"),
939 Output("metadata-hdrh-graph", "children"),
940 Output("offcanvas-metadata", "is_open"),
941 Input("graph-tput", "clickData"),
942 Input("graph-latency", "clickData")
944 def _show_metadata_from_graphs(
945 tput_data: dict, lat_data: dict) -> tuple:
948 if not (tput_data or lat_data):
954 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
955 if trigger_id == "graph-tput":
957 array = tput_data["points"][0]["text"].split("<br>")
960 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
963 elif trigger_id == "graph-latency":
965 array = lat_data["points"][0]["text"].split("<br>")
968 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
971 hdrh_data = lat_data["points"][0].get("customdata", None)
974 class_name="gy-2 p-0",
976 dbc.CardHeader(hdrh_data.pop("name")),
977 dbc.CardBody(children=[
979 id="hdrh-latency-graph",
980 figure=graph_hdrh_latency(
981 hdrh_data, self.layout
989 class_name="gy-2 p-0",
991 dbc.CardHeader(children=[
993 target_id="tput-lat-metadata",
995 style={"display": "inline-block"}
1000 id="tput-lat-metadata",
1003 dbc.ListGroup(children, flush=True)
1010 return metadata, graph, True
1013 Output("download-data", "data"),
1014 State("selected-tests", "data"),
1015 Input("btn-download-data", "n_clicks"),
1016 prevent_initial_call=True
1018 def _download_data(store_sel, n_clicks):
1029 for itm in store_sel:
1030 sel_data = select_trending_data(self.data, itm)
1031 if sel_data is None:
1033 df = pd.concat([df, sel_data], ignore_index=True)
1035 return dcc.send_data_frame(df.to_csv, "trending_data.csv")