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",
474 id="btn-sel-display",
489 def __init__(self, panel: dict) -> None:
497 # Defines also the order of keys
499 "dd-ctrl-phy-value": str(),
500 "dd-ctrl-area-options": list(),
501 "dd-ctrl-area-disabled": True,
502 "dd-ctrl-area-value": str(),
503 "dd-ctrl-test-options": list(),
504 "dd-ctrl-test-disabled": True,
505 "dd-ctrl-test-value": str(),
506 "cl-ctrl-core-options": list(),
507 "cl-ctrl-core-value": list(),
508 "cl-ctrl-core-all-value": list(),
509 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
510 "cl-ctrl-framesize-options": list(),
511 "cl-ctrl-framesize-value": list(),
512 "cl-ctrl-framesize-all-value": list(),
513 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
514 "cl-ctrl-testtype-options": list(),
515 "cl-ctrl-testtype-value": list(),
516 "cl-ctrl-testtype-all-value": list(),
517 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
518 "btn-ctrl-add-disabled": True,
519 "cl-selected-options": list(),
522 self._panel = deepcopy(self._defaults)
524 for key in self._defaults:
525 self._panel[key] = panel[key]
528 def defaults(self) -> dict:
529 return self._defaults
532 def panel(self) -> dict:
535 def set(self, kwargs: dict) -> None:
536 for key, val in kwargs.items():
537 if key in self._panel:
538 self._panel[key] = val
540 raise KeyError(f"The key {key} is not defined.")
542 def get(self, key: str) -> any:
543 return self._panel[key]
545 def values(self) -> tuple:
546 return tuple(self._panel.values())
549 def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
552 options = {v["value"] for v in opt}
554 sel = list(options) if all else list()
556 all = ["all", ] if set(sel) == options else list()
560 def _list_tests(selection: dict) -> list:
561 """Display selected tests with checkboxes
565 {"label": v["id"], "value": v["id"]} for v in selection
570 def callbacks(self, app):
572 def _generate_plotting_arrea(args: tuple) -> tuple:
576 (fig_tput, fig_lat) = args
578 row_fig_tput = self.PLACEHOLDER
579 row_fig_lat = self.PLACEHOLDER
580 row_btn_dwnld = self.PLACEHOLDER
592 dcc.Loading(children=[
594 id="btn-download-data",
595 children=["Download Data"]
597 dcc.Download(id="download-data")
610 return row_fig_tput, row_fig_lat, row_btn_dwnld
613 Output("control-panel", "data"), # Store
614 Output("selected-tests", "data"), # Store
615 Output("row-graph-tput", "children"),
616 Output("row-graph-lat", "children"),
617 Output("row-btn-download", "children"),
618 Output("row-card-sel-tests", "style"),
619 Output("row-btns-sel-tests", "style"),
620 Output("dd-ctrl-phy", "value"),
621 Output("dd-ctrl-area", "options"),
622 Output("dd-ctrl-area", "disabled"),
623 Output("dd-ctrl-area", "value"),
624 Output("dd-ctrl-test", "options"),
625 Output("dd-ctrl-test", "disabled"),
626 Output("dd-ctrl-test", "value"),
627 Output("cl-ctrl-core", "options"),
628 Output("cl-ctrl-core", "value"),
629 Output("cl-ctrl-core-all", "value"),
630 Output("cl-ctrl-core-all", "options"),
631 Output("cl-ctrl-framesize", "options"),
632 Output("cl-ctrl-framesize", "value"),
633 Output("cl-ctrl-framesize-all", "value"),
634 Output("cl-ctrl-framesize-all", "options"),
635 Output("cl-ctrl-testtype", "options"),
636 Output("cl-ctrl-testtype", "value"),
637 Output("cl-ctrl-testtype-all", "value"),
638 Output("cl-ctrl-testtype-all", "options"),
639 Output("btn-ctrl-add", "disabled"),
640 Output("cl-selected", "options"), # User selection
641 State("control-panel", "data"), # Store
642 State("selected-tests", "data"), # Store
643 State("cl-selected", "value"), # User selection
644 Input("dd-ctrl-phy", "value"),
645 Input("dd-ctrl-area", "value"),
646 Input("dd-ctrl-test", "value"),
647 Input("cl-ctrl-core", "value"),
648 Input("cl-ctrl-core-all", "value"),
649 Input("cl-ctrl-framesize", "value"),
650 Input("cl-ctrl-framesize-all", "value"),
651 Input("cl-ctrl-testtype", "value"),
652 Input("cl-ctrl-testtype-all", "value"),
653 Input("btn-ctrl-add", "n_clicks"),
654 Input("dpr-period", "start_date"),
655 Input("dpr-period", "end_date"),
656 Input("btn-sel-display", "n_clicks"),
657 Input("btn-sel-remove", "n_clicks"),
658 Input("btn-sel-remove-all", "n_clicks"),
660 def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
661 dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
662 cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
663 cl_testtype: list, cl_testtype_all: list, btn_add: int,
664 d_start: str, d_end: str, btn_display: int, btn_remove: int,
665 btn_remove_all: int) -> tuple:
669 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
671 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
673 row_fig_tput = no_update
674 row_fig_lat = no_update
675 row_btn_dwnld = no_update
676 row_card_sel_tests = no_update
677 row_btns_sel_tests = no_update
679 ctrl_panel = self.ControlPanel(cp_data)
681 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
683 if trigger_id == "dd-ctrl-phy":
686 {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
687 for v in [v for v in self.spec_tbs[dd_phy].keys()]
694 "dd-ctrl-phy-value": dd_phy,
695 "dd-ctrl-area-value": str(),
696 "dd-ctrl-area-options": options,
697 "dd-ctrl-area-disabled": disabled,
698 "dd-ctrl-test-options": list(),
699 "dd-ctrl-test-disabled": True,
700 "cl-ctrl-core-options": list(),
701 "cl-ctrl-core-value": list(),
702 "cl-ctrl-core-all-value": list(),
703 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
704 "cl-ctrl-framesize-options": list(),
705 "cl-ctrl-framesize-value": list(),
706 "cl-ctrl-framesize-all-value": list(),
707 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
708 "cl-ctrl-testtype-options": list(),
709 "cl-ctrl-testtype-value": list(),
710 "cl-ctrl-testtype-all-value": list(),
711 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
712 "btn-ctrl-add-disabled": True,
714 elif trigger_id == "dd-ctrl-area":
716 phy = ctrl_panel.get("dd-ctrl-phy-value")
718 {"label": v, "value": v}
719 for v in self.spec_tbs[phy][dd_area]["test"]
726 "dd-ctrl-area-value": dd_area,
727 "dd-ctrl-test-value": str(),
728 "dd-ctrl-test-options": options,
729 "dd-ctrl-test-disabled": disabled,
730 "cl-ctrl-core-options": list(),
731 "cl-ctrl-core-value": list(),
732 "cl-ctrl-core-all-value": list(),
733 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
734 "cl-ctrl-framesize-options": list(),
735 "cl-ctrl-framesize-value": list(),
736 "cl-ctrl-framesize-all-value": list(),
737 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
738 "cl-ctrl-testtype-options": list(),
739 "cl-ctrl-testtype-value": list(),
740 "cl-ctrl-testtype-all-value": list(),
741 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
742 "btn-ctrl-add-disabled": True,
744 elif trigger_id == "dd-ctrl-test":
746 framesize_opts = list()
747 testtype_opts = list()
748 phy = ctrl_panel.get("dd-ctrl-phy-value")
749 area = ctrl_panel.get("dd-ctrl-area-value")
750 if phy and area and dd_test:
752 {"label": v, "value": v}
753 for v in self.spec_tbs[phy][area]["core"]
756 {"label": v, "value": v}
757 for v in self.spec_tbs[phy][area]["frame-size"]
760 {"label": v, "value": v}
761 for v in self.spec_tbs[phy][area]["test-type"]
764 "dd-ctrl-test-value": dd_test,
765 "cl-ctrl-core-options": core_opts,
766 "cl-ctrl-core-value": list(),
767 "cl-ctrl-core-all-value": list(),
768 "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
769 "cl-ctrl-framesize-options": framesize_opts,
770 "cl-ctrl-framesize-value": list(),
771 "cl-ctrl-framesize-all-value": list(),
772 "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
773 "cl-ctrl-testtype-options": testtype_opts,
774 "cl-ctrl-testtype-value": list(),
775 "cl-ctrl-testtype-all-value": list(),
776 "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
777 "btn-ctrl-add-disabled": False,
779 elif trigger_id == "cl-ctrl-core":
780 val_sel, val_all = self._sync_checklists(
781 opt=ctrl_panel.get("cl-ctrl-core-options"),
787 "cl-ctrl-core-value": val_sel,
788 "cl-ctrl-core-all-value": val_all,
790 elif trigger_id == "cl-ctrl-core-all":
791 val_sel, val_all = self._sync_checklists(
792 opt = ctrl_panel.get("cl-ctrl-core-options"),
798 "cl-ctrl-core-value": val_sel,
799 "cl-ctrl-core-all-value": val_all,
801 elif trigger_id == "cl-ctrl-framesize":
802 val_sel, val_all = self._sync_checklists(
803 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
809 "cl-ctrl-framesize-value": val_sel,
810 "cl-ctrl-framesize-all-value": val_all,
812 elif trigger_id == "cl-ctrl-framesize-all":
813 val_sel, val_all = self._sync_checklists(
814 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
816 all=cl_framesize_all,
820 "cl-ctrl-framesize-value": val_sel,
821 "cl-ctrl-framesize-all-value": val_all,
823 elif trigger_id == "cl-ctrl-testtype":
824 val_sel, val_all = self._sync_checklists(
825 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
831 "cl-ctrl-testtype-value": val_sel,
832 "cl-ctrl-testtype-all-value": val_all,
834 elif trigger_id == "cl-ctrl-testtype-all":
835 val_sel, val_all = self._sync_checklists(
836 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
842 "cl-ctrl-testtype-value": val_sel,
843 "cl-ctrl-testtype-all-value": val_all,
845 elif trigger_id == "btn-ctrl-add":
847 phy = ctrl_panel.get("dd-ctrl-phy-value")
848 area = ctrl_panel.get("dd-ctrl-area-value")
849 test = ctrl_panel.get("dd-ctrl-test-value")
850 cores = ctrl_panel.get("cl-ctrl-core-value")
851 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
852 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
853 # Add selected test to the list of tests in store:
854 if phy and area and test and cores and framesizes and testtypes:
855 if store_sel is None:
858 for framesize in framesizes:
859 for ttype in testtypes:
861 f"{phy.replace('af_xdp', 'af-xdp')}-"
863 f"{framesize.lower()}-"
868 if tid not in [itm["id"] for itm in store_sel]:
874 "framesize": framesize.lower(),
875 "core": core.lower(),
876 "testtype": ttype.lower()
878 row_card_sel_tests = self.STYLE_ENABLED
879 row_btns_sel_tests = self.STYLE_ENABLED
880 ctrl_panel.set(ctrl_panel.defaults)
882 "cl-selected-options": self._list_tests(store_sel)
884 elif trigger_id in ("btn-sel-display", "dpr-period"):
886 row_fig_tput, row_fig_lat, row_btn_dwnld = \
887 _generate_plotting_arrea(
889 self.data, store_sel, self.layout, d_start, d_end
892 elif trigger_id == "btn-sel-remove-all":
894 row_fig_tput = self.PLACEHOLDER
895 row_fig_lat = self.PLACEHOLDER
896 row_btn_dwnld = self.PLACEHOLDER
897 row_card_sel_tests = self.STYLE_DISABLED
898 row_btns_sel_tests = self.STYLE_DISABLED
901 "cl-selected-options": list()
903 elif trigger_id == "btn-sel-remove":
906 new_store_sel = list()
907 for item in store_sel:
908 if item["id"] not in list_sel:
909 new_store_sel.append(item)
910 store_sel = new_store_sel
912 row_fig_tput, row_fig_lat, row_btn_dwnld = \
913 _generate_plotting_arrea(
915 self.data, store_sel, self.layout, d_start, d_end
919 "cl-selected-options": self._list_tests(store_sel)
922 row_fig_tput = self.PLACEHOLDER
923 row_fig_lat = self.PLACEHOLDER
924 row_btn_dwnld = self.PLACEHOLDER
925 row_card_sel_tests = self.STYLE_DISABLED
926 row_btns_sel_tests = self.STYLE_DISABLED
929 "cl-selected-options": list()
933 ctrl_panel.panel, store_sel,
934 row_fig_tput, row_fig_lat, row_btn_dwnld,
935 row_card_sel_tests, row_btns_sel_tests
937 ret_val.extend(ctrl_panel.values())
941 Output("metadata-tput-lat", "children"),
942 Output("metadata-hdrh-graph", "children"),
943 Output("offcanvas-metadata", "is_open"),
944 Input("graph-tput", "clickData"),
945 Input("graph-latency", "clickData")
947 def _show_metadata_from_graphs(
948 tput_data: dict, lat_data: dict) -> tuple:
951 if not (tput_data or lat_data):
957 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
958 if trigger_id == "graph-tput":
960 array = tput_data["points"][0]["text"].split("<br>")
963 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
966 elif trigger_id == "graph-latency":
968 array = lat_data["points"][0]["text"].split("<br>")
971 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
974 hdrh_data = lat_data["points"][0].get("customdata", None)
977 class_name="gy-2 p-0",
979 dbc.CardHeader(hdrh_data.pop("name")),
980 dbc.CardBody(children=[
982 id="hdrh-latency-graph",
983 figure=graph_hdrh_latency(
984 hdrh_data, self.layout
992 class_name="gy-2 p-0",
994 dbc.CardHeader(children=[
996 target_id="tput-lat-metadata",
998 style={"display": "inline-block"}
1003 id="tput-lat-metadata",
1006 dbc.ListGroup(children, flush=True)
1013 return metadata, graph, True
1016 Output("download-data", "data"),
1017 State("selected-tests", "data"),
1018 Input("btn-download-data", "n_clicks"),
1019 prevent_initial_call=True
1021 def _download_data(store_sel, n_clicks):
1032 for itm in store_sel:
1033 sel_data = select_trending_data(self.data, itm)
1034 if sel_data is None:
1036 df = pd.concat([df, sel_data], ignore_index=True)
1038 return dcc.send_data_frame(df.to_csv, "trending_data.csv")