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 def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
59 self._html_layout_file = html_layout_file
60 self._spec_file = spec_file
61 self._graph_layout_file = graph_layout_file
62 self._data_spec_file = data_spec_file
66 data_spec_file=self._data_spec_file,
68 ).read_trending_mrr(days=5)
71 data_spec_file=self._data_spec_file,
73 ).read_trending_ndrpdr(days=14)
75 self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
78 self._html_layout = ""
80 self._graph_layout = None
83 with open(self._html_layout_file, "r") as file_read:
84 self._html_layout = file_read.read()
85 except IOError as err:
87 f"Not possible to open the file {self._html_layout_file}\n{err}"
91 with open(self._spec_file, "r") as file_read:
92 self._spec_tbs = load(file_read, Loader=FullLoader)
93 except IOError as err:
95 f"Not possible to open the file {self._spec_file,}\n{err}"
97 except YAMLError as err:
99 f"An error occurred while parsing the specification file "
100 f"{self._spec_file,}\n"
105 with open(self._graph_layout_file, "r") as file_read:
106 self._graph_layout = load(file_read, Loader=FullLoader)
107 except IOError as err:
109 f"Not possible to open the file {self._graph_layout_file}\n"
112 except YAMLError as err:
114 f"An error occurred while parsing the specification file "
115 f"{self._graph_layout_file}\n"
120 if self._app is not None and hasattr(self, 'callbacks'):
121 self.callbacks(self._app)
124 def html_layout(self):
125 return self._html_layout
129 return self._spec_tbs
137 return self._graph_layout
139 def add_content(self):
142 if self.html_layout and self.spec_tbs:
155 id="offcanvas-metadata",
156 title="Throughput And Latency",
160 dbc.Row(id="metadata-tput-lat"),
161 dbc.Row(id="metadata-hdrh-graph"),
175 self._add_ctrl_col(),
176 self._add_plotting_col(),
194 def _add_navbar(self):
195 """Add nav element with navigation panel. It is placed on the top.
197 return dbc.NavbarSimple(
198 id="navbarsimple-main",
202 "Continuous Performance Trending",
211 brand_external_link=True,
216 def _add_ctrl_col(self) -> dbc.Col:
217 """Add column with controls. It is placed on the left side.
222 self._add_ctrl_panel(),
226 def _add_plotting_col(self) -> dbc.Col:
227 """Add column with plots and tables. It is placed on the right side.
230 id="col-plotting-area",
232 dbc.Row( # Throughput
234 class_name="g-0 p-2",
237 dcc.Graph(id="graph-tput")
243 class_name="g-0 p-2",
246 dcc.Graph(id="graph-latency")
254 dcc.Loading(children=[
256 id="btn-download-data",
257 children=["Download Data"]
259 dcc.Download(id="download-data")
267 def _add_ctrl_panel(self) -> dbc.Row:
272 class_name="g-0 p-2",
278 "Physical Test Bed Topology, NIC and Driver",
283 placeholder="Select a Physical Test Bed Topology...",
285 {"label": k, "value": k} for k in self.spec_tbs.keys()
300 placeholder="Select an Area...",
315 placeholder="Select a Test...",
332 id="cl-ctrl-core-all",
333 options=self.CL_ALL_DISABLED,
352 id="row-ctrl-framesize",
362 id="cl-ctrl-framesize-all",
363 options=self.CL_ALL_DISABLED,
373 id="cl-ctrl-framesize",
382 id="row-ctrl-testtype",
392 id="cl-ctrl-testtype-all",
393 options=self.CL_ALL_DISABLED,
403 id="cl-ctrl-testtype",
418 children="Add Selected",
432 datetime.utcnow()-timedelta(days=180),
433 max_date_allowed=datetime.utcnow(),
434 initial_visible_month=datetime.utcnow(),
435 start_date=datetime.utcnow() - timedelta(days=180),
436 end_date=datetime.utcnow(),
437 display_format="D MMMM YY"
467 id="btn-sel-remove-all",
468 children="Remove All",
475 children="Remove Selected",
481 id="btn-sel-display",
496 def __init__(self, panel: dict) -> None:
504 # Defines also the order of keys
506 "dd-ctrl-phy-value": str(),
507 "dd-ctrl-area-options": list(),
508 "dd-ctrl-area-disabled": True,
509 "dd-ctrl-area-value": str(),
510 "dd-ctrl-test-options": list(),
511 "dd-ctrl-test-disabled": True,
512 "dd-ctrl-test-value": str(),
513 "cl-ctrl-core-options": list(),
514 "cl-ctrl-core-value": list(),
515 "cl-ctrl-core-all-value": list(),
516 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
517 "cl-ctrl-framesize-options": list(),
518 "cl-ctrl-framesize-value": list(),
519 "cl-ctrl-framesize-all-value": list(),
520 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
521 "cl-ctrl-testtype-options": list(),
522 "cl-ctrl-testtype-value": list(),
523 "cl-ctrl-testtype-all-value": list(),
524 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
525 "btn-ctrl-add-disabled": True,
526 "cl-selected-options": list(),
529 self._panel = deepcopy(self._defaults)
531 for key in self._defaults:
532 self._panel[key] = panel[key]
535 def defaults(self) -> dict:
536 return self._defaults
539 def panel(self) -> dict:
542 def set(self, kwargs: dict) -> None:
543 for key, val in kwargs.items():
544 if key in self._panel:
545 self._panel[key] = val
547 raise KeyError(f"The key {key} is not defined.")
549 def get(self, key: str) -> any:
550 return self._panel[key]
552 def values(self) -> tuple:
553 return tuple(self._panel.values())
556 def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
559 options = {v["value"] for v in opt}
561 sel = list(options) if all else list()
563 all = ["all", ] if set(sel) == options else list()
567 def _list_tests(selection: dict) -> list:
568 """Display selected tests with checkboxes
572 {"label": v["id"], "value": v["id"]} for v in selection
577 def callbacks(self, app):
580 Output("control-panel", "data"), # Store
581 Output("selected-tests", "data"), # Store
582 Output("graph-tput", "figure"),
583 Output("graph-latency", "figure"),
584 Output("dd-ctrl-phy", "value"),
585 Output("dd-ctrl-area", "options"),
586 Output("dd-ctrl-area", "disabled"),
587 Output("dd-ctrl-area", "value"),
588 Output("dd-ctrl-test", "options"),
589 Output("dd-ctrl-test", "disabled"),
590 Output("dd-ctrl-test", "value"),
591 Output("cl-ctrl-core", "options"),
592 Output("cl-ctrl-core", "value"),
593 Output("cl-ctrl-core-all", "value"),
594 Output("cl-ctrl-core-all", "options"),
595 Output("cl-ctrl-framesize", "options"),
596 Output("cl-ctrl-framesize", "value"),
597 Output("cl-ctrl-framesize-all", "value"),
598 Output("cl-ctrl-framesize-all", "options"),
599 Output("cl-ctrl-testtype", "options"),
600 Output("cl-ctrl-testtype", "value"),
601 Output("cl-ctrl-testtype-all", "value"),
602 Output("cl-ctrl-testtype-all", "options"),
603 Output("btn-ctrl-add", "disabled"),
604 Output("cl-selected", "options"), # User selection
605 State("control-panel", "data"), # Store
606 State("selected-tests", "data"), # Store
607 State("cl-selected", "value"), # User selection
608 Input("dd-ctrl-phy", "value"),
609 Input("dd-ctrl-area", "value"),
610 Input("dd-ctrl-test", "value"),
611 Input("cl-ctrl-core", "value"),
612 Input("cl-ctrl-core-all", "value"),
613 Input("cl-ctrl-framesize", "value"),
614 Input("cl-ctrl-framesize-all", "value"),
615 Input("cl-ctrl-testtype", "value"),
616 Input("cl-ctrl-testtype-all", "value"),
617 Input("btn-ctrl-add", "n_clicks"),
618 Input("dpr-period", "start_date"),
619 Input("dpr-period", "end_date"),
620 Input("btn-sel-display", "n_clicks"),
621 Input("btn-sel-remove", "n_clicks"),
622 Input("btn-sel-remove-all", "n_clicks"),
624 def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
625 dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
626 cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
627 cl_testtype: list, cl_testtype_all: list, btn_add: int,
628 d_start: str, d_end: str, btn_display: int, btn_remove: int,
629 btn_remove_all: int) -> tuple:
633 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
635 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
640 ctrl_panel = self.ControlPanel(cp_data)
642 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
644 if trigger_id == "dd-ctrl-phy":
647 {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
648 for v in [v for v in self.spec_tbs[dd_phy].keys()]
655 "dd-ctrl-phy-value": dd_phy,
656 "dd-ctrl-area-value": str(),
657 "dd-ctrl-area-options": options,
658 "dd-ctrl-area-disabled": disabled,
659 "dd-ctrl-test-options": list(),
660 "dd-ctrl-test-disabled": True,
661 "cl-ctrl-core-options": list(),
662 "cl-ctrl-core-value": list(),
663 "cl-ctrl-core-all-value": list(),
664 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
665 "cl-ctrl-framesize-options": list(),
666 "cl-ctrl-framesize-value": list(),
667 "cl-ctrl-framesize-all-value": list(),
668 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
669 "cl-ctrl-testtype-options": list(),
670 "cl-ctrl-testtype-value": list(),
671 "cl-ctrl-testtype-all-value": list(),
672 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
673 "btn-ctrl-add-disabled": True,
675 elif trigger_id == "dd-ctrl-area":
677 phy = ctrl_panel.get("dd-ctrl-phy-value")
679 {"label": v, "value": v}
680 for v in self.spec_tbs[phy][dd_area]["test"]
687 "dd-ctrl-area-value": dd_area,
688 "dd-ctrl-test-value": str(),
689 "dd-ctrl-test-options": options,
690 "dd-ctrl-test-disabled": disabled,
691 "cl-ctrl-core-options": list(),
692 "cl-ctrl-core-value": list(),
693 "cl-ctrl-core-all-value": list(),
694 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
695 "cl-ctrl-framesize-options": list(),
696 "cl-ctrl-framesize-value": list(),
697 "cl-ctrl-framesize-all-value": list(),
698 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
699 "cl-ctrl-testtype-options": list(),
700 "cl-ctrl-testtype-value": list(),
701 "cl-ctrl-testtype-all-value": list(),
702 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
703 "btn-ctrl-add-disabled": True,
705 elif trigger_id == "dd-ctrl-test":
707 framesize_opts = list()
708 testtype_opts = list()
709 phy = ctrl_panel.get("dd-ctrl-phy-value")
710 area = ctrl_panel.get("dd-ctrl-area-value")
711 if phy and area and dd_test:
713 {"label": v, "value": v}
714 for v in self.spec_tbs[phy][area]["core"]
717 {"label": v, "value": v}
718 for v in self.spec_tbs[phy][area]["frame-size"]
721 {"label": v, "value": v}
722 for v in self.spec_tbs[phy][area]["test-type"]
725 "dd-ctrl-test-value": dd_test,
726 "cl-ctrl-core-options": core_opts,
727 "cl-ctrl-core-value": list(),
728 "cl-ctrl-core-all-value": list(),
729 "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
730 "cl-ctrl-framesize-options": framesize_opts,
731 "cl-ctrl-framesize-value": list(),
732 "cl-ctrl-framesize-all-value": list(),
733 "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
734 "cl-ctrl-testtype-options": testtype_opts,
735 "cl-ctrl-testtype-value": list(),
736 "cl-ctrl-testtype-all-value": list(),
737 "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
738 "btn-ctrl-add-disabled": False,
740 elif trigger_id == "cl-ctrl-core":
741 val_sel, val_all = self._sync_checklists(
742 opt=ctrl_panel.get("cl-ctrl-core-options"),
748 "cl-ctrl-core-value": val_sel,
749 "cl-ctrl-core-all-value": val_all,
751 elif trigger_id == "cl-ctrl-core-all":
752 val_sel, val_all = self._sync_checklists(
753 opt = ctrl_panel.get("cl-ctrl-core-options"),
759 "cl-ctrl-core-value": val_sel,
760 "cl-ctrl-core-all-value": val_all,
762 elif trigger_id == "cl-ctrl-framesize":
763 val_sel, val_all = self._sync_checklists(
764 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
770 "cl-ctrl-framesize-value": val_sel,
771 "cl-ctrl-framesize-all-value": val_all,
773 elif trigger_id == "cl-ctrl-framesize-all":
774 val_sel, val_all = self._sync_checklists(
775 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
777 all=cl_framesize_all,
781 "cl-ctrl-framesize-value": val_sel,
782 "cl-ctrl-framesize-all-value": val_all,
784 elif trigger_id == "cl-ctrl-testtype":
785 val_sel, val_all = self._sync_checklists(
786 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
792 "cl-ctrl-testtype-value": val_sel,
793 "cl-ctrl-testtype-all-value": val_all,
795 elif trigger_id == "cl-ctrl-testtype-all":
796 val_sel, val_all = self._sync_checklists(
797 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
803 "cl-ctrl-testtype-value": val_sel,
804 "cl-ctrl-testtype-all-value": val_all,
806 elif trigger_id == "btn-ctrl-add":
808 phy = ctrl_panel.get("dd-ctrl-phy-value")
809 area = ctrl_panel.get("dd-ctrl-area-value")
810 test = ctrl_panel.get("dd-ctrl-test-value")
811 cores = ctrl_panel.get("cl-ctrl-core-value")
812 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
813 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
814 # Add selected test to the list of tests in store:
815 if phy and area and test and cores and framesizes and testtypes:
816 if store_sel is None:
819 for framesize in framesizes:
820 for ttype in testtypes:
822 f"{phy.replace('af_xdp', 'af-xdp')}-"
824 f"{framesize.lower()}-"
829 if tid not in [itm["id"] for itm in store_sel]:
835 "framesize": framesize.lower(),
836 "core": core.lower(),
837 "testtype": ttype.lower()
839 ctrl_panel.set(ctrl_panel.defaults)
841 "cl-selected-options": self._list_tests(store_sel)
843 elif trigger_id in ("btn-sel-display", "dpr-period"):
845 fig_tput, fig_lat = graph_trending(
846 self.data, store_sel, self.layout, d_start, d_end
848 fig_tput = fig_tput if fig_tput else self.NO_GRAPH
849 fig_lat = fig_lat if fig_lat else self.NO_GRAPH
850 elif trigger_id == "btn-sel-remove-all":
852 fig_tput = self.NO_GRAPH
853 fig_lat = self.NO_GRAPH
856 "cl-selected-options": list()
858 elif trigger_id == "btn-sel-remove":
861 new_store_sel = list()
862 for item in store_sel:
863 if item["id"] not in list_sel:
864 new_store_sel.append(item)
865 store_sel = new_store_sel
867 fig_tput, fig_lat = graph_trending(
868 self.data, store_sel, self.layout, d_start, d_end
870 fig_tput = fig_tput if fig_tput else self.NO_GRAPH
871 fig_lat = fig_lat if fig_lat else self.NO_GRAPH
873 "cl-selected-options": self._list_tests(store_sel)
876 fig_tput = self.NO_GRAPH
877 fig_lat = self.NO_GRAPH
880 "cl-selected-options": list()
883 ret_val = [ctrl_panel.panel, store_sel, fig_tput, fig_lat]
884 ret_val.extend(ctrl_panel.values())
888 Output("metadata-tput-lat", "children"),
889 Output("metadata-hdrh-graph", "children"),
890 Output("offcanvas-metadata", "is_open"),
891 Input("graph-tput", "clickData"),
892 Input("graph-latency", "clickData")
894 def _show_metadata_from_graphs(
895 tput_data: dict, lat_data: dict) -> tuple:
898 if not (tput_data or lat_data):
904 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
905 if trigger_id == "graph-tput":
907 txt = tput_data["points"][0]["text"].replace("<br>", "\n")
908 elif trigger_id == "graph-latency":
910 txt = lat_data["points"][0]["text"].replace("<br>", "\n")
911 hdrh_data = lat_data["points"][0].get("customdata", None)
914 id="hdrh-latency-graph",
915 figure=graph_hdrh_latency(hdrh_data, self.layout)
922 dbc.CardHeader(children=[
924 target_id="tput-lat-metadata",
926 style={"display": "inline-block"}
931 id="tput-lat-metadata",
938 return metadata, graph, True
941 Output("download-data", "data"),
942 State("selected-tests", "data"),
943 Input("btn-download-data", "n_clicks"),
944 prevent_initial_call=True
946 def _download_data(store_sel, n_clicks):
954 for itm in store_sel:
955 sel_data = select_trending_data(self.data, itm)
958 df = pd.concat([df, sel_data], ignore_index=True)
960 return dcc.send_data_frame(df.to_csv, "trending_data.csv")