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:
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"
434 id="row-card-sel-tests",
436 style=self.STYLE_DISABLED,
443 class_name="overflow-auto",
447 style={"max-height": "12em"},
452 id="row-btns-sel-tests",
453 style=self.STYLE_DISABLED,
458 id="btn-sel-remove-all",
459 children="Remove All",
466 children="Remove Selected",
472 id="btn-sel-display",
487 def __init__(self, panel: dict) -> None:
495 # Defines also the order of keys
497 "dd-ctrl-phy-value": str(),
498 "dd-ctrl-area-options": list(),
499 "dd-ctrl-area-disabled": True,
500 "dd-ctrl-area-value": str(),
501 "dd-ctrl-test-options": list(),
502 "dd-ctrl-test-disabled": True,
503 "dd-ctrl-test-value": str(),
504 "cl-ctrl-core-options": list(),
505 "cl-ctrl-core-value": list(),
506 "cl-ctrl-core-all-value": list(),
507 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
508 "cl-ctrl-framesize-options": list(),
509 "cl-ctrl-framesize-value": list(),
510 "cl-ctrl-framesize-all-value": list(),
511 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
512 "cl-ctrl-testtype-options": list(),
513 "cl-ctrl-testtype-value": list(),
514 "cl-ctrl-testtype-all-value": list(),
515 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
516 "btn-ctrl-add-disabled": True,
517 "cl-selected-options": list(),
520 self._panel = deepcopy(self._defaults)
522 for key in self._defaults:
523 self._panel[key] = panel[key]
526 def defaults(self) -> dict:
527 return self._defaults
530 def panel(self) -> dict:
533 def set(self, kwargs: dict) -> None:
534 for key, val in kwargs.items():
535 if key in self._panel:
536 self._panel[key] = val
538 raise KeyError(f"The key {key} is not defined.")
540 def get(self, key: str) -> any:
541 return self._panel[key]
543 def values(self) -> tuple:
544 return tuple(self._panel.values())
547 def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
550 options = {v["value"] for v in opt}
552 sel = list(options) if all else list()
554 all = ["all", ] if set(sel) == options else list()
558 def _list_tests(selection: dict) -> list:
559 """Display selected tests with checkboxes
563 {"label": v["id"], "value": v["id"]} for v in selection
568 def callbacks(self, app):
570 def _generate_plotting_arrea(args: tuple) -> tuple:
574 (fig_tput, fig_lat) = args
576 row_fig_tput = self.PLACEHOLDER
577 row_fig_lat = self.PLACEHOLDER
578 row_btn_dwnld = self.PLACEHOLDER
590 dcc.Loading(children=[
592 id="btn-download-data",
593 children=["Download Data"]
595 dcc.Download(id="download-data")
608 return row_fig_tput, row_fig_lat, row_btn_dwnld
611 Output("control-panel", "data"), # Store
612 Output("selected-tests", "data"), # Store
613 Output("row-graph-tput", "children"),
614 Output("row-graph-lat", "children"),
615 Output("row-btn-download", "children"),
616 Output("row-card-sel-tests", "style"),
617 Output("row-btns-sel-tests", "style"),
618 Output("dd-ctrl-phy", "value"),
619 Output("dd-ctrl-area", "options"),
620 Output("dd-ctrl-area", "disabled"),
621 Output("dd-ctrl-area", "value"),
622 Output("dd-ctrl-test", "options"),
623 Output("dd-ctrl-test", "disabled"),
624 Output("dd-ctrl-test", "value"),
625 Output("cl-ctrl-core", "options"),
626 Output("cl-ctrl-core", "value"),
627 Output("cl-ctrl-core-all", "value"),
628 Output("cl-ctrl-core-all", "options"),
629 Output("cl-ctrl-framesize", "options"),
630 Output("cl-ctrl-framesize", "value"),
631 Output("cl-ctrl-framesize-all", "value"),
632 Output("cl-ctrl-framesize-all", "options"),
633 Output("cl-ctrl-testtype", "options"),
634 Output("cl-ctrl-testtype", "value"),
635 Output("cl-ctrl-testtype-all", "value"),
636 Output("cl-ctrl-testtype-all", "options"),
637 Output("btn-ctrl-add", "disabled"),
638 Output("cl-selected", "options"), # User selection
639 State("control-panel", "data"), # Store
640 State("selected-tests", "data"), # Store
641 State("cl-selected", "value"), # User selection
642 Input("dd-ctrl-phy", "value"),
643 Input("dd-ctrl-area", "value"),
644 Input("dd-ctrl-test", "value"),
645 Input("cl-ctrl-core", "value"),
646 Input("cl-ctrl-core-all", "value"),
647 Input("cl-ctrl-framesize", "value"),
648 Input("cl-ctrl-framesize-all", "value"),
649 Input("cl-ctrl-testtype", "value"),
650 Input("cl-ctrl-testtype-all", "value"),
651 Input("btn-ctrl-add", "n_clicks"),
652 Input("dpr-period", "start_date"),
653 Input("dpr-period", "end_date"),
654 Input("btn-sel-display", "n_clicks"),
655 Input("btn-sel-remove", "n_clicks"),
656 Input("btn-sel-remove-all", "n_clicks"),
658 def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
659 dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
660 cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
661 cl_testtype: list, cl_testtype_all: list, btn_add: int,
662 d_start: str, d_end: str, btn_display: int, btn_remove: int,
663 btn_remove_all: int) -> tuple:
667 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
669 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
671 row_fig_tput = no_update
672 row_fig_lat = no_update
673 row_btn_dwnld = no_update
674 row_card_sel_tests = no_update
675 row_btns_sel_tests = no_update
677 ctrl_panel = self.ControlPanel(cp_data)
679 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
681 if trigger_id == "dd-ctrl-phy":
684 {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
685 for v in [v for v in self.spec_tbs[dd_phy].keys()]
692 "dd-ctrl-phy-value": dd_phy,
693 "dd-ctrl-area-value": str(),
694 "dd-ctrl-area-options": options,
695 "dd-ctrl-area-disabled": disabled,
696 "dd-ctrl-test-options": list(),
697 "dd-ctrl-test-disabled": True,
698 "cl-ctrl-core-options": list(),
699 "cl-ctrl-core-value": list(),
700 "cl-ctrl-core-all-value": list(),
701 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
702 "cl-ctrl-framesize-options": list(),
703 "cl-ctrl-framesize-value": list(),
704 "cl-ctrl-framesize-all-value": list(),
705 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
706 "cl-ctrl-testtype-options": list(),
707 "cl-ctrl-testtype-value": list(),
708 "cl-ctrl-testtype-all-value": list(),
709 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
710 "btn-ctrl-add-disabled": True,
712 elif trigger_id == "dd-ctrl-area":
714 phy = ctrl_panel.get("dd-ctrl-phy-value")
716 {"label": v, "value": v}
717 for v in self.spec_tbs[phy][dd_area]["test"]
724 "dd-ctrl-area-value": dd_area,
725 "dd-ctrl-test-value": str(),
726 "dd-ctrl-test-options": options,
727 "dd-ctrl-test-disabled": disabled,
728 "cl-ctrl-core-options": list(),
729 "cl-ctrl-core-value": list(),
730 "cl-ctrl-core-all-value": list(),
731 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
732 "cl-ctrl-framesize-options": list(),
733 "cl-ctrl-framesize-value": list(),
734 "cl-ctrl-framesize-all-value": list(),
735 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
736 "cl-ctrl-testtype-options": list(),
737 "cl-ctrl-testtype-value": list(),
738 "cl-ctrl-testtype-all-value": list(),
739 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
740 "btn-ctrl-add-disabled": True,
742 elif trigger_id == "dd-ctrl-test":
744 framesize_opts = list()
745 testtype_opts = list()
746 phy = ctrl_panel.get("dd-ctrl-phy-value")
747 area = ctrl_panel.get("dd-ctrl-area-value")
748 if phy and area and dd_test:
750 {"label": v, "value": v}
751 for v in self.spec_tbs[phy][area]["core"]
754 {"label": v, "value": v}
755 for v in self.spec_tbs[phy][area]["frame-size"]
758 {"label": v, "value": v}
759 for v in self.spec_tbs[phy][area]["test-type"]
762 "dd-ctrl-test-value": dd_test,
763 "cl-ctrl-core-options": core_opts,
764 "cl-ctrl-core-value": list(),
765 "cl-ctrl-core-all-value": list(),
766 "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
767 "cl-ctrl-framesize-options": framesize_opts,
768 "cl-ctrl-framesize-value": list(),
769 "cl-ctrl-framesize-all-value": list(),
770 "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
771 "cl-ctrl-testtype-options": testtype_opts,
772 "cl-ctrl-testtype-value": list(),
773 "cl-ctrl-testtype-all-value": list(),
774 "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
775 "btn-ctrl-add-disabled": False,
777 elif trigger_id == "cl-ctrl-core":
778 val_sel, val_all = self._sync_checklists(
779 opt=ctrl_panel.get("cl-ctrl-core-options"),
785 "cl-ctrl-core-value": val_sel,
786 "cl-ctrl-core-all-value": val_all,
788 elif trigger_id == "cl-ctrl-core-all":
789 val_sel, val_all = self._sync_checklists(
790 opt = ctrl_panel.get("cl-ctrl-core-options"),
796 "cl-ctrl-core-value": val_sel,
797 "cl-ctrl-core-all-value": val_all,
799 elif trigger_id == "cl-ctrl-framesize":
800 val_sel, val_all = self._sync_checklists(
801 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
807 "cl-ctrl-framesize-value": val_sel,
808 "cl-ctrl-framesize-all-value": val_all,
810 elif trigger_id == "cl-ctrl-framesize-all":
811 val_sel, val_all = self._sync_checklists(
812 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
814 all=cl_framesize_all,
818 "cl-ctrl-framesize-value": val_sel,
819 "cl-ctrl-framesize-all-value": val_all,
821 elif trigger_id == "cl-ctrl-testtype":
822 val_sel, val_all = self._sync_checklists(
823 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
829 "cl-ctrl-testtype-value": val_sel,
830 "cl-ctrl-testtype-all-value": val_all,
832 elif trigger_id == "cl-ctrl-testtype-all":
833 val_sel, val_all = self._sync_checklists(
834 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
840 "cl-ctrl-testtype-value": val_sel,
841 "cl-ctrl-testtype-all-value": val_all,
843 elif trigger_id == "btn-ctrl-add":
845 phy = ctrl_panel.get("dd-ctrl-phy-value")
846 area = ctrl_panel.get("dd-ctrl-area-value")
847 test = ctrl_panel.get("dd-ctrl-test-value")
848 cores = ctrl_panel.get("cl-ctrl-core-value")
849 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
850 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
851 # Add selected test to the list of tests in store:
852 if phy and area and test and cores and framesizes and testtypes:
853 if store_sel is None:
856 for framesize in framesizes:
857 for ttype in testtypes:
859 f"{phy.replace('af_xdp', 'af-xdp')}-"
861 f"{framesize.lower()}-"
866 if tid not in [itm["id"] for itm in store_sel]:
872 "framesize": framesize.lower(),
873 "core": core.lower(),
874 "testtype": ttype.lower()
876 row_card_sel_tests = self.STYLE_ENABLED
877 row_btns_sel_tests = self.STYLE_ENABLED
878 ctrl_panel.set(ctrl_panel.defaults)
880 "cl-selected-options": self._list_tests(store_sel)
882 elif trigger_id in ("btn-sel-display", "dpr-period"):
884 row_fig_tput, row_fig_lat, row_btn_dwnld = \
885 _generate_plotting_arrea(
887 self.data, store_sel, self.layout, d_start, d_end
890 elif trigger_id == "btn-sel-remove-all":
892 row_fig_tput = self.PLACEHOLDER
893 row_fig_lat = self.PLACEHOLDER
894 row_btn_dwnld = self.PLACEHOLDER
895 row_card_sel_tests = self.STYLE_DISABLED
896 row_btns_sel_tests = self.STYLE_DISABLED
899 "cl-selected-options": list()
901 elif trigger_id == "btn-sel-remove":
904 new_store_sel = list()
905 for item in store_sel:
906 if item["id"] not in list_sel:
907 new_store_sel.append(item)
908 store_sel = new_store_sel
910 row_fig_tput, row_fig_lat, row_btn_dwnld = \
911 _generate_plotting_arrea(
913 self.data, store_sel, self.layout, d_start, d_end
917 "cl-selected-options": self._list_tests(store_sel)
920 row_fig_tput = self.PLACEHOLDER
921 row_fig_lat = self.PLACEHOLDER
922 row_btn_dwnld = self.PLACEHOLDER
923 row_card_sel_tests = self.STYLE_DISABLED
924 row_btns_sel_tests = self.STYLE_DISABLED
927 "cl-selected-options": list()
931 ctrl_panel.panel, store_sel,
932 row_fig_tput, row_fig_lat, row_btn_dwnld,
933 row_card_sel_tests, row_btns_sel_tests
935 ret_val.extend(ctrl_panel.values())
939 Output("metadata-tput-lat", "children"),
940 Output("metadata-hdrh-graph", "children"),
941 Output("offcanvas-metadata", "is_open"),
942 Input("graph-tput", "clickData"),
943 Input("graph-latency", "clickData")
945 def _show_metadata_from_graphs(
946 tput_data: dict, lat_data: dict) -> tuple:
949 if not (tput_data or lat_data):
955 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
956 if trigger_id == "graph-tput":
958 txt = tput_data["points"][0]["text"].replace("<br>", "\n")
959 elif trigger_id == "graph-latency":
961 txt = lat_data["points"][0]["text"].replace("<br>", "\n")
962 hdrh_data = lat_data["points"][0].get("customdata", None)
967 dbc.CardHeader(hdrh_data.pop("name")),
968 dbc.CardBody(children=[
970 id="hdrh-latency-graph",
971 figure=graph_hdrh_latency(
972 hdrh_data, self.layout
982 dbc.CardHeader(children=[
984 target_id="tput-lat-metadata",
986 style={"display": "inline-block"}
991 id="tput-lat-metadata",
998 return metadata, graph, True
1001 Output("download-data", "data"),
1002 State("selected-tests", "data"),
1003 Input("btn-download-data", "n_clicks"),
1004 prevent_initial_call=True
1006 def _download_data(store_sel, n_clicks):
1017 for itm in store_sel:
1018 sel_data = select_trending_data(self.data, itm)
1019 if sel_data is None:
1021 df = pd.concat([df, sel_data], ignore_index=True)
1023 return dcc.send_data_frame(df.to_csv, "trending_data.csv")