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 NO_GRAPH = {"data": [], "layout": {}, "frames": []}
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:
157 id="offcanvas-metadata",
158 title="Throughput And Latency",
162 dbc.Row(id="metadata-tput-lat"),
163 dbc.Row(id="metadata-hdrh-graph"),
177 self._add_ctrl_col(),
178 self._add_plotting_col(),
196 def _add_navbar(self):
197 """Add nav element with navigation panel. It is placed on the top.
199 return dbc.NavbarSimple(
200 id="navbarsimple-main",
204 "Continuous Performance Trending",
213 brand_external_link=True,
218 def _add_ctrl_col(self) -> dbc.Col:
219 """Add column with controls. It is placed on the left side.
224 self._add_ctrl_panel(),
228 def _add_plotting_col(self) -> dbc.Col:
229 """Add column with plots and tables. It is placed on the right side.
232 id="col-plotting-area",
234 dbc.Row( # Throughput
236 class_name="g-0 p-2",
243 class_name="g-0 p-2",
249 id="row-btn-download",
250 class_name="g-0 p-2",
259 def _add_ctrl_panel(self) -> dbc.Row:
264 class_name="g-0 p-2",
270 "Physical Test Bed Topology, NIC and Driver",
275 placeholder="Select a Physical Test Bed Topology...",
277 {"label": k, "value": k} for k in self.spec_tbs.keys()
292 placeholder="Select an Area...",
307 placeholder="Select a Test...",
324 id="cl-ctrl-core-all",
325 options=self.CL_ALL_DISABLED,
344 id="row-ctrl-framesize",
354 id="cl-ctrl-framesize-all",
355 options=self.CL_ALL_DISABLED,
365 id="cl-ctrl-framesize",
374 id="row-ctrl-testtype",
384 id="cl-ctrl-testtype-all",
385 options=self.CL_ALL_DISABLED,
395 id="cl-ctrl-testtype",
410 children="Add Selected",
424 datetime.utcnow()-timedelta(days=180),
425 max_date_allowed=datetime.utcnow(),
426 initial_visible_month=datetime.utcnow(),
427 start_date=datetime.utcnow() - timedelta(days=180),
428 end_date=datetime.utcnow(),
429 display_format="D MMMM YY"
459 id="btn-sel-remove-all",
460 children="Remove All",
467 children="Remove Selected",
473 id="btn-sel-display",
488 def __init__(self, panel: dict) -> None:
496 # Defines also the order of keys
498 "dd-ctrl-phy-value": str(),
499 "dd-ctrl-area-options": list(),
500 "dd-ctrl-area-disabled": True,
501 "dd-ctrl-area-value": str(),
502 "dd-ctrl-test-options": list(),
503 "dd-ctrl-test-disabled": True,
504 "dd-ctrl-test-value": str(),
505 "cl-ctrl-core-options": list(),
506 "cl-ctrl-core-value": list(),
507 "cl-ctrl-core-all-value": list(),
508 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
509 "cl-ctrl-framesize-options": list(),
510 "cl-ctrl-framesize-value": list(),
511 "cl-ctrl-framesize-all-value": list(),
512 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
513 "cl-ctrl-testtype-options": list(),
514 "cl-ctrl-testtype-value": list(),
515 "cl-ctrl-testtype-all-value": list(),
516 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
517 "btn-ctrl-add-disabled": True,
518 "cl-selected-options": list(),
521 self._panel = deepcopy(self._defaults)
523 for key in self._defaults:
524 self._panel[key] = panel[key]
527 def defaults(self) -> dict:
528 return self._defaults
531 def panel(self) -> dict:
534 def set(self, kwargs: dict) -> None:
535 for key, val in kwargs.items():
536 if key in self._panel:
537 self._panel[key] = val
539 raise KeyError(f"The key {key} is not defined.")
541 def get(self, key: str) -> any:
542 return self._panel[key]
544 def values(self) -> tuple:
545 return tuple(self._panel.values())
548 def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
551 options = {v["value"] for v in opt}
553 sel = list(options) if all else list()
555 all = ["all", ] if set(sel) == options else list()
559 def _list_tests(selection: dict) -> list:
560 """Display selected tests with checkboxes
564 {"label": v["id"], "value": v["id"]} for v in selection
569 def callbacks(self, app):
571 def _generate_plotting_arrea(args: tuple) -> tuple:
575 (fig_tput, fig_lat) = args
577 row_fig_tput = self.PLACEHOLDER
578 row_fig_lat = self.PLACEHOLDER
579 row_btn_dwnld = self.PLACEHOLDER
591 dcc.Loading(children=[
593 id="btn-download-data",
594 children=["Download Data"]
596 dcc.Download(id="download-data")
609 return row_fig_tput, row_fig_lat, row_btn_dwnld
612 Output("control-panel", "data"), # Store
613 Output("selected-tests", "data"), # Store
614 Output("row-graph-tput", "children"),
615 Output("row-graph-lat", "children"),
616 Output("row-btn-download", "children"),
617 Output("dd-ctrl-phy", "value"),
618 Output("dd-ctrl-area", "options"),
619 Output("dd-ctrl-area", "disabled"),
620 Output("dd-ctrl-area", "value"),
621 Output("dd-ctrl-test", "options"),
622 Output("dd-ctrl-test", "disabled"),
623 Output("dd-ctrl-test", "value"),
624 Output("cl-ctrl-core", "options"),
625 Output("cl-ctrl-core", "value"),
626 Output("cl-ctrl-core-all", "value"),
627 Output("cl-ctrl-core-all", "options"),
628 Output("cl-ctrl-framesize", "options"),
629 Output("cl-ctrl-framesize", "value"),
630 Output("cl-ctrl-framesize-all", "value"),
631 Output("cl-ctrl-framesize-all", "options"),
632 Output("cl-ctrl-testtype", "options"),
633 Output("cl-ctrl-testtype", "value"),
634 Output("cl-ctrl-testtype-all", "value"),
635 Output("cl-ctrl-testtype-all", "options"),
636 Output("btn-ctrl-add", "disabled"),
637 Output("cl-selected", "options"), # User selection
638 State("control-panel", "data"), # Store
639 State("selected-tests", "data"), # Store
640 State("cl-selected", "value"), # User selection
641 Input("dd-ctrl-phy", "value"),
642 Input("dd-ctrl-area", "value"),
643 Input("dd-ctrl-test", "value"),
644 Input("cl-ctrl-core", "value"),
645 Input("cl-ctrl-core-all", "value"),
646 Input("cl-ctrl-framesize", "value"),
647 Input("cl-ctrl-framesize-all", "value"),
648 Input("cl-ctrl-testtype", "value"),
649 Input("cl-ctrl-testtype-all", "value"),
650 Input("btn-ctrl-add", "n_clicks"),
651 Input("dpr-period", "start_date"),
652 Input("dpr-period", "end_date"),
653 Input("btn-sel-display", "n_clicks"),
654 Input("btn-sel-remove", "n_clicks"),
655 Input("btn-sel-remove-all", "n_clicks"),
657 def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
658 dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
659 cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
660 cl_testtype: list, cl_testtype_all: list, btn_add: int,
661 d_start: str, d_end: str, btn_display: int, btn_remove: int,
662 btn_remove_all: int) -> tuple:
666 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
668 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
670 row_fig_tput = no_update
671 row_fig_lat = no_update
672 row_btn_dwnld = no_update
674 ctrl_panel = self.ControlPanel(cp_data)
676 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
678 if trigger_id == "dd-ctrl-phy":
681 {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
682 for v in [v for v in self.spec_tbs[dd_phy].keys()]
689 "dd-ctrl-phy-value": dd_phy,
690 "dd-ctrl-area-value": str(),
691 "dd-ctrl-area-options": options,
692 "dd-ctrl-area-disabled": disabled,
693 "dd-ctrl-test-options": list(),
694 "dd-ctrl-test-disabled": True,
695 "cl-ctrl-core-options": list(),
696 "cl-ctrl-core-value": list(),
697 "cl-ctrl-core-all-value": list(),
698 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
699 "cl-ctrl-framesize-options": list(),
700 "cl-ctrl-framesize-value": list(),
701 "cl-ctrl-framesize-all-value": list(),
702 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
703 "cl-ctrl-testtype-options": list(),
704 "cl-ctrl-testtype-value": list(),
705 "cl-ctrl-testtype-all-value": list(),
706 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
707 "btn-ctrl-add-disabled": True,
709 elif trigger_id == "dd-ctrl-area":
711 phy = ctrl_panel.get("dd-ctrl-phy-value")
713 {"label": v, "value": v}
714 for v in self.spec_tbs[phy][dd_area]["test"]
721 "dd-ctrl-area-value": dd_area,
722 "dd-ctrl-test-value": str(),
723 "dd-ctrl-test-options": options,
724 "dd-ctrl-test-disabled": disabled,
725 "cl-ctrl-core-options": list(),
726 "cl-ctrl-core-value": list(),
727 "cl-ctrl-core-all-value": list(),
728 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
729 "cl-ctrl-framesize-options": list(),
730 "cl-ctrl-framesize-value": list(),
731 "cl-ctrl-framesize-all-value": list(),
732 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
733 "cl-ctrl-testtype-options": list(),
734 "cl-ctrl-testtype-value": list(),
735 "cl-ctrl-testtype-all-value": list(),
736 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
737 "btn-ctrl-add-disabled": True,
739 elif trigger_id == "dd-ctrl-test":
741 framesize_opts = list()
742 testtype_opts = list()
743 phy = ctrl_panel.get("dd-ctrl-phy-value")
744 area = ctrl_panel.get("dd-ctrl-area-value")
745 if phy and area and dd_test:
747 {"label": v, "value": v}
748 for v in self.spec_tbs[phy][area]["core"]
751 {"label": v, "value": v}
752 for v in self.spec_tbs[phy][area]["frame-size"]
755 {"label": v, "value": v}
756 for v in self.spec_tbs[phy][area]["test-type"]
759 "dd-ctrl-test-value": dd_test,
760 "cl-ctrl-core-options": core_opts,
761 "cl-ctrl-core-value": list(),
762 "cl-ctrl-core-all-value": list(),
763 "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
764 "cl-ctrl-framesize-options": framesize_opts,
765 "cl-ctrl-framesize-value": list(),
766 "cl-ctrl-framesize-all-value": list(),
767 "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
768 "cl-ctrl-testtype-options": testtype_opts,
769 "cl-ctrl-testtype-value": list(),
770 "cl-ctrl-testtype-all-value": list(),
771 "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
772 "btn-ctrl-add-disabled": False,
774 elif trigger_id == "cl-ctrl-core":
775 val_sel, val_all = self._sync_checklists(
776 opt=ctrl_panel.get("cl-ctrl-core-options"),
782 "cl-ctrl-core-value": val_sel,
783 "cl-ctrl-core-all-value": val_all,
785 elif trigger_id == "cl-ctrl-core-all":
786 val_sel, val_all = self._sync_checklists(
787 opt = ctrl_panel.get("cl-ctrl-core-options"),
793 "cl-ctrl-core-value": val_sel,
794 "cl-ctrl-core-all-value": val_all,
796 elif trigger_id == "cl-ctrl-framesize":
797 val_sel, val_all = self._sync_checklists(
798 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
804 "cl-ctrl-framesize-value": val_sel,
805 "cl-ctrl-framesize-all-value": val_all,
807 elif trigger_id == "cl-ctrl-framesize-all":
808 val_sel, val_all = self._sync_checklists(
809 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
811 all=cl_framesize_all,
815 "cl-ctrl-framesize-value": val_sel,
816 "cl-ctrl-framesize-all-value": val_all,
818 elif trigger_id == "cl-ctrl-testtype":
819 val_sel, val_all = self._sync_checklists(
820 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
826 "cl-ctrl-testtype-value": val_sel,
827 "cl-ctrl-testtype-all-value": val_all,
829 elif trigger_id == "cl-ctrl-testtype-all":
830 val_sel, val_all = self._sync_checklists(
831 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
837 "cl-ctrl-testtype-value": val_sel,
838 "cl-ctrl-testtype-all-value": val_all,
840 elif trigger_id == "btn-ctrl-add":
842 phy = ctrl_panel.get("dd-ctrl-phy-value")
843 area = ctrl_panel.get("dd-ctrl-area-value")
844 test = ctrl_panel.get("dd-ctrl-test-value")
845 cores = ctrl_panel.get("cl-ctrl-core-value")
846 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
847 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
848 # Add selected test to the list of tests in store:
849 if phy and area and test and cores and framesizes and testtypes:
850 if store_sel is None:
853 for framesize in framesizes:
854 for ttype in testtypes:
856 f"{phy.replace('af_xdp', 'af-xdp')}-"
858 f"{framesize.lower()}-"
863 if tid not in [itm["id"] for itm in store_sel]:
869 "framesize": framesize.lower(),
870 "core": core.lower(),
871 "testtype": ttype.lower()
873 ctrl_panel.set(ctrl_panel.defaults)
875 "cl-selected-options": self._list_tests(store_sel)
877 elif trigger_id in ("btn-sel-display", "dpr-period"):
879 row_fig_tput, row_fig_lat, row_btn_dwnld = \
880 _generate_plotting_arrea(
882 self.data, store_sel, self.layout, d_start, d_end
885 elif trigger_id == "btn-sel-remove-all":
887 row_fig_tput = self.PLACEHOLDER
888 row_fig_lat = self.PLACEHOLDER
889 row_btn_dwnld = self.PLACEHOLDER
892 "cl-selected-options": list()
894 elif trigger_id == "btn-sel-remove":
897 new_store_sel = list()
898 for item in store_sel:
899 if item["id"] not in list_sel:
900 new_store_sel.append(item)
901 store_sel = new_store_sel
903 row_fig_tput, row_fig_lat, row_btn_dwnld = \
904 _generate_plotting_arrea(
906 self.data, store_sel, self.layout, d_start, d_end
910 "cl-selected-options": self._list_tests(store_sel)
913 row_fig_tput = self.PLACEHOLDER
914 row_fig_lat = self.PLACEHOLDER
915 row_btn_dwnld = self.PLACEHOLDER
918 "cl-selected-options": list()
922 ctrl_panel.panel, store_sel,
923 row_fig_tput, row_fig_lat, row_btn_dwnld
925 ret_val.extend(ctrl_panel.values())
929 Output("metadata-tput-lat", "children"),
930 Output("metadata-hdrh-graph", "children"),
931 Output("offcanvas-metadata", "is_open"),
932 Input("graph-tput", "clickData"),
933 Input("graph-latency", "clickData")
935 def _show_metadata_from_graphs(
936 tput_data: dict, lat_data: dict) -> tuple:
939 if not (tput_data or lat_data):
945 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
946 if trigger_id == "graph-tput":
948 txt = tput_data["points"][0]["text"].replace("<br>", "\n")
949 elif trigger_id == "graph-latency":
951 txt = lat_data["points"][0]["text"].replace("<br>", "\n")
952 hdrh_data = lat_data["points"][0].get("customdata", None)
957 dbc.CardHeader(hdrh_data.pop("name")),
958 dbc.CardBody(children=[
960 id="hdrh-latency-graph",
961 figure=graph_hdrh_latency(
962 hdrh_data, self.layout
972 dbc.CardHeader(children=[
974 target_id="tput-lat-metadata",
976 style={"display": "inline-block"}
981 id="tput-lat-metadata",
988 return metadata, graph, True
991 Output("download-data", "data"),
992 State("selected-tests", "data"),
993 Input("btn-download-data", "n_clicks"),
994 prevent_initial_call=True
996 def _download_data(store_sel, n_clicks):
1007 for itm in store_sel:
1008 sel_data = select_trending_data(self.data, itm)
1009 if sel_data is None:
1011 df = pd.concat([df, sel_data], ignore_index=True)
1013 return dcc.send_data_frame(df.to_csv, "trending_data.csv")