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, ALL
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
28 from json import loads, JSONDecodeError
30 from ..data.data import Data
31 from .graphs import graph_trending, graph_hdrh_latency, \
39 STYLE_DISABLED = {"display": "none"}
40 STYLE_ENABLED = {"display": "inherit"}
53 PLACEHOLDER = html.Nobr("")
55 def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
62 self._html_layout_file = html_layout_file
63 self._spec_file = spec_file
64 self._graph_layout_file = graph_layout_file
65 self._data_spec_file = data_spec_file
69 data_spec_file=self._data_spec_file,
71 ).read_trending_mrr(days=5)
74 data_spec_file=self._data_spec_file,
76 ).read_trending_ndrpdr(days=14)
78 self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
81 self._html_layout = ""
83 self._graph_layout = None
86 with open(self._html_layout_file, "r") as file_read:
87 self._html_layout = file_read.read()
88 except IOError as err:
90 f"Not possible to open the file {self._html_layout_file}\n{err}"
94 with open(self._spec_file, "r") as file_read:
95 self._spec_tbs = load(file_read, Loader=FullLoader)
96 except IOError as err:
98 f"Not possible to open the file {self._spec_file,}\n{err}"
100 except YAMLError as err:
102 f"An error occurred while parsing the specification file "
103 f"{self._spec_file,}\n"
108 with open(self._graph_layout_file, "r") as file_read:
109 self._graph_layout = load(file_read, Loader=FullLoader)
110 except IOError as err:
112 f"Not possible to open the file {self._graph_layout_file}\n"
115 except YAMLError as err:
117 f"An error occurred while parsing the specification file "
118 f"{self._graph_layout_file}\n"
123 if self._app is not None and hasattr(self, 'callbacks'):
124 self.callbacks(self._app)
127 def html_layout(self):
128 return self._html_layout
132 return self._spec_tbs
140 return self._graph_layout
142 def add_content(self):
145 if self.html_layout and self.spec_tbs:
159 id="offcanvas-metadata",
160 title="Throughput And Latency",
164 dbc.Row(id="metadata-tput-lat"),
165 dbc.Row(id="metadata-hdrh-graph"),
179 self._add_ctrl_col(),
180 self._add_plotting_col(),
198 def _add_navbar(self):
199 """Add nav element with navigation panel. It is placed on the top.
201 return dbc.NavbarSimple(
202 id="navbarsimple-main",
206 "Continuous Performance Trending",
215 brand_external_link=True,
220 def _add_ctrl_col(self) -> dbc.Col:
221 """Add column with controls. It is placed on the left side.
226 self._add_ctrl_panel(),
230 def _add_plotting_col(self) -> dbc.Col:
231 """Add column with plots and tables. It is placed on the right side.
234 id="col-plotting-area",
236 dbc.Row( # Throughput
238 class_name="g-0 p-2",
245 class_name="g-0 p-2",
251 id="row-btn-download",
252 class_name="g-0 p-2",
261 def _add_ctrl_panel(self) -> dbc.Row:
266 class_name="g-0 p-2",
272 "Physical Test Bed Topology, NIC and Driver",
277 placeholder="Select a Physical Test Bed Topology...",
279 {"label": k, "value": k} for k in self.spec_tbs.keys()
294 placeholder="Select an Area...",
309 placeholder="Select a Test...",
326 id="cl-ctrl-core-all",
327 options=self.CL_ALL_DISABLED,
346 id="row-ctrl-framesize",
356 id="cl-ctrl-framesize-all",
357 options=self.CL_ALL_DISABLED,
367 id="cl-ctrl-framesize",
376 id="row-ctrl-testtype",
386 id="cl-ctrl-testtype-all",
387 options=self.CL_ALL_DISABLED,
397 id="cl-ctrl-testtype",
412 children="Add Selected",
425 className="d-flex justify-content-center",
427 datetime.utcnow()-timedelta(days=180),
428 max_date_allowed=datetime.utcnow(),
429 initial_visible_month=datetime.utcnow(),
430 start_date=datetime.utcnow() - timedelta(days=180),
431 end_date=datetime.utcnow(),
432 display_format="D MMMM YY"
437 id="row-card-sel-tests",
439 style=self.STYLE_DISABLED,
446 class_name="overflow-auto",
450 style={"max-height": "12em"},
455 id="row-btns-sel-tests",
456 style=self.STYLE_DISABLED,
461 id="btn-sel-remove-all",
462 children="Remove All",
469 children="Remove Selected",
483 def __init__(self, panel: dict) -> None:
491 # Defines also the order of keys
493 "dd-ctrl-phy-value": str(),
494 "dd-ctrl-area-options": list(),
495 "dd-ctrl-area-disabled": True,
496 "dd-ctrl-area-value": str(),
497 "dd-ctrl-test-options": list(),
498 "dd-ctrl-test-disabled": True,
499 "dd-ctrl-test-value": str(),
500 "cl-ctrl-core-options": list(),
501 "cl-ctrl-core-value": list(),
502 "cl-ctrl-core-all-value": list(),
503 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
504 "cl-ctrl-framesize-options": list(),
505 "cl-ctrl-framesize-value": list(),
506 "cl-ctrl-framesize-all-value": list(),
507 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
508 "cl-ctrl-testtype-options": list(),
509 "cl-ctrl-testtype-value": list(),
510 "cl-ctrl-testtype-all-value": list(),
511 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
512 "btn-ctrl-add-disabled": True,
513 "cl-selected-options": list(),
516 self._panel = deepcopy(self._defaults)
518 for key in self._defaults:
519 self._panel[key] = panel[key]
522 def defaults(self) -> dict:
523 return self._defaults
526 def panel(self) -> dict:
529 def set(self, kwargs: dict) -> None:
530 for key, val in kwargs.items():
531 if key in self._panel:
532 self._panel[key] = val
534 raise KeyError(f"The key {key} is not defined.")
536 def get(self, key: str) -> any:
537 return self._panel[key]
539 def values(self) -> tuple:
540 return tuple(self._panel.values())
543 def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
546 options = {v["value"] for v in opt}
548 sel = list(options) if all else list()
550 all = ["all", ] if set(sel) == options else list()
554 def _list_tests(selection: dict) -> list:
555 """Display selected tests with checkboxes
559 {"label": v["id"], "value": v["id"]} for v in selection
564 def callbacks(self, app):
566 def _generate_plotting_arrea(args: tuple) -> tuple:
570 (fig_tput, fig_lat) = args
572 row_fig_tput = self.PLACEHOLDER
573 row_fig_lat = self.PLACEHOLDER
574 row_btn_dwnld = self.PLACEHOLDER
580 id={"type": "graph", "index": "tput"},
586 dcc.Loading(children=[
588 id="btn-download-data",
589 children=["Download Data"]
591 dcc.Download(id="download-data")
598 id={"type": "graph", "index": "lat"},
604 return row_fig_tput, row_fig_lat, row_btn_dwnld
607 Output("control-panel", "data"), # Store
608 Output("selected-tests", "data"), # Store
609 Output("row-graph-tput", "children"),
610 Output("row-graph-lat", "children"),
611 Output("row-btn-download", "children"),
612 Output("row-card-sel-tests", "style"),
613 Output("row-btns-sel-tests", "style"),
614 Output("dd-ctrl-phy", "value"),
615 Output("dd-ctrl-area", "options"),
616 Output("dd-ctrl-area", "disabled"),
617 Output("dd-ctrl-area", "value"),
618 Output("dd-ctrl-test", "options"),
619 Output("dd-ctrl-test", "disabled"),
620 Output("dd-ctrl-test", "value"),
621 Output("cl-ctrl-core", "options"),
622 Output("cl-ctrl-core", "value"),
623 Output("cl-ctrl-core-all", "value"),
624 Output("cl-ctrl-core-all", "options"),
625 Output("cl-ctrl-framesize", "options"),
626 Output("cl-ctrl-framesize", "value"),
627 Output("cl-ctrl-framesize-all", "value"),
628 Output("cl-ctrl-framesize-all", "options"),
629 Output("cl-ctrl-testtype", "options"),
630 Output("cl-ctrl-testtype", "value"),
631 Output("cl-ctrl-testtype-all", "value"),
632 Output("cl-ctrl-testtype-all", "options"),
633 Output("btn-ctrl-add", "disabled"),
634 Output("cl-selected", "options"), # User selection
635 State("control-panel", "data"), # Store
636 State("selected-tests", "data"), # Store
637 State("cl-selected", "value"), # User selection
638 Input("dd-ctrl-phy", "value"),
639 Input("dd-ctrl-area", "value"),
640 Input("dd-ctrl-test", "value"),
641 Input("cl-ctrl-core", "value"),
642 Input("cl-ctrl-core-all", "value"),
643 Input("cl-ctrl-framesize", "value"),
644 Input("cl-ctrl-framesize-all", "value"),
645 Input("cl-ctrl-testtype", "value"),
646 Input("cl-ctrl-testtype-all", "value"),
647 Input("btn-ctrl-add", "n_clicks"),
648 Input("dpr-period", "start_date"),
649 Input("dpr-period", "end_date"),
650 Input("btn-sel-remove", "n_clicks"),
651 Input("btn-sel-remove-all", "n_clicks"),
653 def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
654 dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
655 cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
656 cl_testtype: list, cl_testtype_all: list, btn_add: int,
657 d_start: str, d_end: str, btn_remove: int,
658 btn_remove_all: int) -> tuple:
662 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
664 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
666 row_fig_tput = no_update
667 row_fig_lat = no_update
668 row_btn_dwnld = no_update
669 row_card_sel_tests = no_update
670 row_btns_sel_tests = no_update
672 ctrl_panel = self.ControlPanel(cp_data)
674 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
676 if trigger_id == "dd-ctrl-phy":
679 {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
680 for v in [v for v in self.spec_tbs[dd_phy].keys()]
687 "dd-ctrl-phy-value": dd_phy,
688 "dd-ctrl-area-value": str(),
689 "dd-ctrl-area-options": options,
690 "dd-ctrl-area-disabled": disabled,
691 "dd-ctrl-test-options": list(),
692 "dd-ctrl-test-disabled": True,
693 "cl-ctrl-core-options": list(),
694 "cl-ctrl-core-value": list(),
695 "cl-ctrl-core-all-value": list(),
696 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
697 "cl-ctrl-framesize-options": list(),
698 "cl-ctrl-framesize-value": list(),
699 "cl-ctrl-framesize-all-value": list(),
700 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
701 "cl-ctrl-testtype-options": list(),
702 "cl-ctrl-testtype-value": list(),
703 "cl-ctrl-testtype-all-value": list(),
704 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
705 "btn-ctrl-add-disabled": True,
707 elif trigger_id == "dd-ctrl-area":
709 phy = ctrl_panel.get("dd-ctrl-phy-value")
711 {"label": v, "value": v}
712 for v in self.spec_tbs[phy][dd_area]["test"]
719 "dd-ctrl-area-value": dd_area,
720 "dd-ctrl-test-value": str(),
721 "dd-ctrl-test-options": options,
722 "dd-ctrl-test-disabled": disabled,
723 "cl-ctrl-core-options": list(),
724 "cl-ctrl-core-value": list(),
725 "cl-ctrl-core-all-value": list(),
726 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
727 "cl-ctrl-framesize-options": list(),
728 "cl-ctrl-framesize-value": list(),
729 "cl-ctrl-framesize-all-value": list(),
730 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
731 "cl-ctrl-testtype-options": list(),
732 "cl-ctrl-testtype-value": list(),
733 "cl-ctrl-testtype-all-value": list(),
734 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
735 "btn-ctrl-add-disabled": True,
737 elif trigger_id == "dd-ctrl-test":
739 framesize_opts = list()
740 testtype_opts = list()
741 phy = ctrl_panel.get("dd-ctrl-phy-value")
742 area = ctrl_panel.get("dd-ctrl-area-value")
743 if phy and area and dd_test:
745 {"label": v, "value": v}
746 for v in self.spec_tbs[phy][area]["core"]
749 {"label": v, "value": v}
750 for v in self.spec_tbs[phy][area]["frame-size"]
753 {"label": v, "value": v}
754 for v in self.spec_tbs[phy][area]["test-type"]
757 "dd-ctrl-test-value": dd_test,
758 "cl-ctrl-core-options": core_opts,
759 "cl-ctrl-core-value": list(),
760 "cl-ctrl-core-all-value": list(),
761 "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
762 "cl-ctrl-framesize-options": framesize_opts,
763 "cl-ctrl-framesize-value": list(),
764 "cl-ctrl-framesize-all-value": list(),
765 "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
766 "cl-ctrl-testtype-options": testtype_opts,
767 "cl-ctrl-testtype-value": list(),
768 "cl-ctrl-testtype-all-value": list(),
769 "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
770 "btn-ctrl-add-disabled": False,
772 elif trigger_id == "cl-ctrl-core":
773 val_sel, val_all = self._sync_checklists(
774 opt=ctrl_panel.get("cl-ctrl-core-options"),
780 "cl-ctrl-core-value": val_sel,
781 "cl-ctrl-core-all-value": val_all,
783 elif trigger_id == "cl-ctrl-core-all":
784 val_sel, val_all = self._sync_checklists(
785 opt = ctrl_panel.get("cl-ctrl-core-options"),
791 "cl-ctrl-core-value": val_sel,
792 "cl-ctrl-core-all-value": val_all,
794 elif trigger_id == "cl-ctrl-framesize":
795 val_sel, val_all = self._sync_checklists(
796 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
802 "cl-ctrl-framesize-value": val_sel,
803 "cl-ctrl-framesize-all-value": val_all,
805 elif trigger_id == "cl-ctrl-framesize-all":
806 val_sel, val_all = self._sync_checklists(
807 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
809 all=cl_framesize_all,
813 "cl-ctrl-framesize-value": val_sel,
814 "cl-ctrl-framesize-all-value": val_all,
816 elif trigger_id == "cl-ctrl-testtype":
817 val_sel, val_all = self._sync_checklists(
818 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
824 "cl-ctrl-testtype-value": val_sel,
825 "cl-ctrl-testtype-all-value": val_all,
827 elif trigger_id == "cl-ctrl-testtype-all":
828 val_sel, val_all = self._sync_checklists(
829 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
835 "cl-ctrl-testtype-value": val_sel,
836 "cl-ctrl-testtype-all-value": val_all,
838 elif trigger_id == "btn-ctrl-add":
840 phy = ctrl_panel.get("dd-ctrl-phy-value")
841 area = ctrl_panel.get("dd-ctrl-area-value")
842 test = ctrl_panel.get("dd-ctrl-test-value")
843 cores = ctrl_panel.get("cl-ctrl-core-value")
844 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
845 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
846 # Add selected test to the list of tests in store:
847 if phy and area and test and cores and framesizes and testtypes:
848 if store_sel is None:
851 for framesize in framesizes:
852 for ttype in testtypes:
854 f"{phy.replace('af_xdp', 'af-xdp')}-"
856 f"{framesize.lower()}-"
861 if tid not in [itm["id"] for itm in store_sel]:
867 "framesize": framesize.lower(),
868 "core": core.lower(),
869 "testtype": ttype.lower()
871 row_card_sel_tests = self.STYLE_ENABLED
872 row_btns_sel_tests = self.STYLE_ENABLED
873 ctrl_panel.set(ctrl_panel.defaults)
875 "cl-selected-options": self._list_tests(store_sel)
877 row_fig_tput, row_fig_lat, row_btn_dwnld = \
878 _generate_plotting_arrea(
880 self.data, store_sel, self.layout, d_start, d_end
883 elif trigger_id == "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({"type": "graph", "index": ALL}, "clickData"),
943 prevent_initial_call=True
945 def _show_metadata_from_graphs(graph_data: dict) -> tuple:
950 callback_context.triggered[0]["prop_id"].split(".")[0]
952 idx = 0 if trigger_id == "tput" else 1
953 graph_data = graph_data[idx]["points"][0]
954 except (JSONDecodeError, IndexError, KeyError, ValueError,
963 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
964 ) for x in graph_data.get("text", "").split("<br>")
966 if trigger_id == "tput":
968 elif trigger_id == "lat":
970 hdrh_data = graph_data.get("customdata", None)
973 class_name="gy-2 p-0",
975 dbc.CardHeader(hdrh_data.pop("name")),
976 dbc.CardBody(children=[
978 id="hdrh-latency-graph",
979 figure=graph_hdrh_latency(
980 hdrh_data, self.layout
988 class_name="gy-2 p-0",
990 dbc.CardHeader(children=[
992 target_id="tput-lat-metadata",
994 style={"display": "inline-block"}
999 id="tput-lat-metadata",
1001 children=[dbc.ListGroup(children, flush=True), ]
1007 return metadata, graph, True
1010 Output("download-data", "data"),
1011 State("selected-tests", "data"),
1012 Input("btn-download-data", "n_clicks"),
1013 prevent_initial_call=True
1015 def _download_data(store_sel, n_clicks):
1026 for itm in store_sel:
1027 sel_data = select_trending_data(self.data, itm)
1028 if sel_data is None:
1030 df = pd.concat([df, sel_data], ignore_index=True)
1032 return dcc.send_data_frame(df.to_csv, "trending_data.csv")