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,
74 data_spec_file=self._data_spec_file,
76 ).read_trending_ndrpdr()
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",
273 dbc.InputGroupText("Infra"),
276 placeholder="Select a Physical Test Bed Topology...",
278 {"label": k, "value": k} for k in self.spec_tbs.keys()
292 dbc.InputGroupText("Area"),
295 placeholder="Select an Area...",
309 dbc.InputGroupText("Test"),
312 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",
412 class_name="gy-1 p-0",
418 children="Add Selected",
432 className="d-flex justify-content-center",
434 datetime.utcnow()-timedelta(days=180),
435 max_date_allowed=datetime.utcnow(),
436 initial_visible_month=datetime.utcnow(),
437 start_date=datetime.utcnow() - timedelta(days=180),
438 end_date=datetime.utcnow(),
439 display_format="D MMMM YY"
444 id="row-card-sel-tests",
446 style=self.STYLE_DISABLED,
453 class_name="overflow-auto",
457 style={"max-height": "12em"},
462 id="row-btns-sel-tests",
463 style=self.STYLE_DISABLED,
470 children="Remove Selected",
471 class_name="w-100 me-1",
476 id="btn-sel-remove-all",
477 children="Remove All",
478 class_name="w-100 me-1",
491 def __init__(self, panel: dict) -> None:
499 # Defines also the order of keys
501 "dd-ctrl-phy-value": str(),
502 "dd-ctrl-area-options": list(),
503 "dd-ctrl-area-disabled": True,
504 "dd-ctrl-area-value": str(),
505 "dd-ctrl-test-options": list(),
506 "dd-ctrl-test-disabled": True,
507 "dd-ctrl-test-value": str(),
508 "cl-ctrl-core-options": list(),
509 "cl-ctrl-core-value": list(),
510 "cl-ctrl-core-all-value": list(),
511 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
512 "cl-ctrl-framesize-options": list(),
513 "cl-ctrl-framesize-value": list(),
514 "cl-ctrl-framesize-all-value": list(),
515 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
516 "cl-ctrl-testtype-options": list(),
517 "cl-ctrl-testtype-value": list(),
518 "cl-ctrl-testtype-all-value": list(),
519 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
520 "btn-ctrl-add-disabled": True,
521 "cl-selected-options": list(),
524 self._panel = deepcopy(self._defaults)
526 for key in self._defaults:
527 self._panel[key] = panel[key]
530 def defaults(self) -> dict:
531 return self._defaults
534 def panel(self) -> dict:
537 def set(self, kwargs: dict) -> None:
538 for key, val in kwargs.items():
539 if key in self._panel:
540 self._panel[key] = val
542 raise KeyError(f"The key {key} is not defined.")
544 def get(self, key: str) -> any:
545 return self._panel[key]
547 def values(self) -> tuple:
548 return tuple(self._panel.values())
551 def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
554 options = {v["value"] for v in opt}
556 sel = list(options) if all else list()
558 all = ["all", ] if set(sel) == options else list()
562 def _list_tests(selection: dict) -> list:
563 """Display selected tests with checkboxes
567 {"label": v["id"], "value": v["id"]} for v in selection
572 def callbacks(self, app):
574 def _generate_plotting_arrea(args: tuple) -> tuple:
578 (fig_tput, fig_lat) = args
580 row_fig_tput = self.PLACEHOLDER
581 row_fig_lat = self.PLACEHOLDER
582 row_btn_dwnld = self.PLACEHOLDER
588 id={"type": "graph", "index": "tput"},
594 dcc.Loading(children=[
596 id="btn-download-data",
597 children=["Download Data"],
601 dcc.Download(id="download-data")
608 id={"type": "graph", "index": "lat"},
614 return row_fig_tput, row_fig_lat, row_btn_dwnld
617 Output("control-panel", "data"), # Store
618 Output("selected-tests", "data"), # Store
619 Output("row-graph-tput", "children"),
620 Output("row-graph-lat", "children"),
621 Output("row-btn-download", "children"),
622 Output("row-card-sel-tests", "style"),
623 Output("row-btns-sel-tests", "style"),
624 Output("dd-ctrl-phy", "value"),
625 Output("dd-ctrl-area", "options"),
626 Output("dd-ctrl-area", "disabled"),
627 Output("dd-ctrl-area", "value"),
628 Output("dd-ctrl-test", "options"),
629 Output("dd-ctrl-test", "disabled"),
630 Output("dd-ctrl-test", "value"),
631 Output("cl-ctrl-core", "options"),
632 Output("cl-ctrl-core", "value"),
633 Output("cl-ctrl-core-all", "value"),
634 Output("cl-ctrl-core-all", "options"),
635 Output("cl-ctrl-framesize", "options"),
636 Output("cl-ctrl-framesize", "value"),
637 Output("cl-ctrl-framesize-all", "value"),
638 Output("cl-ctrl-framesize-all", "options"),
639 Output("cl-ctrl-testtype", "options"),
640 Output("cl-ctrl-testtype", "value"),
641 Output("cl-ctrl-testtype-all", "value"),
642 Output("cl-ctrl-testtype-all", "options"),
643 Output("btn-ctrl-add", "disabled"),
644 Output("cl-selected", "options"), # User selection
645 State("control-panel", "data"), # Store
646 State("selected-tests", "data"), # Store
647 State("cl-selected", "value"), # User selection
648 Input("dd-ctrl-phy", "value"),
649 Input("dd-ctrl-area", "value"),
650 Input("dd-ctrl-test", "value"),
651 Input("cl-ctrl-core", "value"),
652 Input("cl-ctrl-core-all", "value"),
653 Input("cl-ctrl-framesize", "value"),
654 Input("cl-ctrl-framesize-all", "value"),
655 Input("cl-ctrl-testtype", "value"),
656 Input("cl-ctrl-testtype-all", "value"),
657 Input("btn-ctrl-add", "n_clicks"),
658 Input("dpr-period", "start_date"),
659 Input("dpr-period", "end_date"),
660 Input("btn-sel-remove", "n_clicks"),
661 Input("btn-sel-remove-all", "n_clicks"),
663 def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
664 dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
665 cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
666 cl_testtype: list, cl_testtype_all: list, btn_add: int,
667 d_start: str, d_end: str, btn_remove: int,
668 btn_remove_all: int) -> tuple:
672 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
674 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
676 row_fig_tput = no_update
677 row_fig_lat = no_update
678 row_btn_dwnld = no_update
679 row_card_sel_tests = no_update
680 row_btns_sel_tests = no_update
682 ctrl_panel = self.ControlPanel(cp_data)
684 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
686 if trigger_id == "dd-ctrl-phy":
689 {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
690 for v in [v for v in self.spec_tbs[dd_phy].keys()]
697 "dd-ctrl-phy-value": dd_phy,
698 "dd-ctrl-area-value": str(),
699 "dd-ctrl-area-options": options,
700 "dd-ctrl-area-disabled": disabled,
701 "dd-ctrl-test-options": list(),
702 "dd-ctrl-test-disabled": True,
703 "cl-ctrl-core-options": list(),
704 "cl-ctrl-core-value": list(),
705 "cl-ctrl-core-all-value": list(),
706 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
707 "cl-ctrl-framesize-options": list(),
708 "cl-ctrl-framesize-value": list(),
709 "cl-ctrl-framesize-all-value": list(),
710 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
711 "cl-ctrl-testtype-options": list(),
712 "cl-ctrl-testtype-value": list(),
713 "cl-ctrl-testtype-all-value": list(),
714 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
715 "btn-ctrl-add-disabled": True,
717 elif trigger_id == "dd-ctrl-area":
719 phy = ctrl_panel.get("dd-ctrl-phy-value")
721 {"label": v, "value": v}
722 for v in self.spec_tbs[phy][dd_area]["test"]
729 "dd-ctrl-area-value": dd_area,
730 "dd-ctrl-test-value": str(),
731 "dd-ctrl-test-options": options,
732 "dd-ctrl-test-disabled": disabled,
733 "cl-ctrl-core-options": list(),
734 "cl-ctrl-core-value": list(),
735 "cl-ctrl-core-all-value": list(),
736 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
737 "cl-ctrl-framesize-options": list(),
738 "cl-ctrl-framesize-value": list(),
739 "cl-ctrl-framesize-all-value": list(),
740 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
741 "cl-ctrl-testtype-options": list(),
742 "cl-ctrl-testtype-value": list(),
743 "cl-ctrl-testtype-all-value": list(),
744 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
745 "btn-ctrl-add-disabled": True,
747 elif trigger_id == "dd-ctrl-test":
749 framesize_opts = list()
750 testtype_opts = list()
751 phy = ctrl_panel.get("dd-ctrl-phy-value")
752 area = ctrl_panel.get("dd-ctrl-area-value")
753 if phy and area and dd_test:
755 {"label": v, "value": v}
756 for v in self.spec_tbs[phy][area]["core"]
759 {"label": v, "value": v}
760 for v in self.spec_tbs[phy][area]["frame-size"]
763 {"label": v, "value": v}
764 for v in self.spec_tbs[phy][area]["test-type"]
767 "dd-ctrl-test-value": dd_test,
768 "cl-ctrl-core-options": core_opts,
769 "cl-ctrl-core-value": list(),
770 "cl-ctrl-core-all-value": list(),
771 "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
772 "cl-ctrl-framesize-options": framesize_opts,
773 "cl-ctrl-framesize-value": list(),
774 "cl-ctrl-framesize-all-value": list(),
775 "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
776 "cl-ctrl-testtype-options": testtype_opts,
777 "cl-ctrl-testtype-value": list(),
778 "cl-ctrl-testtype-all-value": list(),
779 "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
780 "btn-ctrl-add-disabled": False,
782 elif trigger_id == "cl-ctrl-core":
783 val_sel, val_all = self._sync_checklists(
784 opt=ctrl_panel.get("cl-ctrl-core-options"),
790 "cl-ctrl-core-value": val_sel,
791 "cl-ctrl-core-all-value": val_all,
793 elif trigger_id == "cl-ctrl-core-all":
794 val_sel, val_all = self._sync_checklists(
795 opt = ctrl_panel.get("cl-ctrl-core-options"),
801 "cl-ctrl-core-value": val_sel,
802 "cl-ctrl-core-all-value": val_all,
804 elif trigger_id == "cl-ctrl-framesize":
805 val_sel, val_all = self._sync_checklists(
806 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
812 "cl-ctrl-framesize-value": val_sel,
813 "cl-ctrl-framesize-all-value": val_all,
815 elif trigger_id == "cl-ctrl-framesize-all":
816 val_sel, val_all = self._sync_checklists(
817 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
819 all=cl_framesize_all,
823 "cl-ctrl-framesize-value": val_sel,
824 "cl-ctrl-framesize-all-value": val_all,
826 elif trigger_id == "cl-ctrl-testtype":
827 val_sel, val_all = self._sync_checklists(
828 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
834 "cl-ctrl-testtype-value": val_sel,
835 "cl-ctrl-testtype-all-value": val_all,
837 elif trigger_id == "cl-ctrl-testtype-all":
838 val_sel, val_all = self._sync_checklists(
839 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
845 "cl-ctrl-testtype-value": val_sel,
846 "cl-ctrl-testtype-all-value": val_all,
848 elif trigger_id == "btn-ctrl-add":
850 phy = ctrl_panel.get("dd-ctrl-phy-value")
851 area = ctrl_panel.get("dd-ctrl-area-value")
852 test = ctrl_panel.get("dd-ctrl-test-value")
853 cores = ctrl_panel.get("cl-ctrl-core-value")
854 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
855 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
856 # Add selected test to the list of tests in store:
857 if phy and area and test and cores and framesizes and testtypes:
858 if store_sel is None:
861 for framesize in framesizes:
862 for ttype in testtypes:
864 f"{phy.replace('af_xdp', 'af-xdp')}-"
866 f"{framesize.lower()}-"
871 if tid not in [itm["id"] for itm in store_sel]:
877 "framesize": framesize.lower(),
878 "core": core.lower(),
879 "testtype": ttype.lower()
881 row_card_sel_tests = self.STYLE_ENABLED
882 row_btns_sel_tests = self.STYLE_ENABLED
883 ctrl_panel.set(ctrl_panel.defaults)
885 "cl-selected-options": self._list_tests(store_sel)
887 row_fig_tput, row_fig_lat, row_btn_dwnld = \
888 _generate_plotting_arrea(
890 self.data, store_sel, self.layout, d_start, d_end
893 elif trigger_id == "dpr-period":
894 row_fig_tput, row_fig_lat, row_btn_dwnld = \
895 _generate_plotting_arrea(
897 self.data, store_sel, self.layout, d_start, d_end
900 elif trigger_id == "btn-sel-remove-all":
902 row_fig_tput = self.PLACEHOLDER
903 row_fig_lat = self.PLACEHOLDER
904 row_btn_dwnld = self.PLACEHOLDER
905 row_card_sel_tests = self.STYLE_DISABLED
906 row_btns_sel_tests = self.STYLE_DISABLED
909 "cl-selected-options": list()
911 elif trigger_id == "btn-sel-remove":
914 new_store_sel = list()
915 for item in store_sel:
916 if item["id"] not in list_sel:
917 new_store_sel.append(item)
918 store_sel = new_store_sel
920 row_fig_tput, row_fig_lat, row_btn_dwnld = \
921 _generate_plotting_arrea(
923 self.data, store_sel, self.layout, d_start, d_end
927 "cl-selected-options": self._list_tests(store_sel)
930 row_fig_tput = self.PLACEHOLDER
931 row_fig_lat = self.PLACEHOLDER
932 row_btn_dwnld = self.PLACEHOLDER
933 row_card_sel_tests = self.STYLE_DISABLED
934 row_btns_sel_tests = self.STYLE_DISABLED
937 "cl-selected-options": list()
941 ctrl_panel.panel, store_sel,
942 row_fig_tput, row_fig_lat, row_btn_dwnld,
943 row_card_sel_tests, row_btns_sel_tests
945 ret_val.extend(ctrl_panel.values())
949 Output("metadata-tput-lat", "children"),
950 Output("metadata-hdrh-graph", "children"),
951 Output("offcanvas-metadata", "is_open"),
952 Input({"type": "graph", "index": ALL}, "clickData"),
953 prevent_initial_call=True
955 def _show_metadata_from_graphs(graph_data: dict) -> tuple:
960 callback_context.triggered[0]["prop_id"].split(".")[0]
962 idx = 0 if trigger_id == "tput" else 1
963 graph_data = graph_data[idx]["points"][0]
964 except (JSONDecodeError, IndexError, KeyError, ValueError,
973 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
974 ) for x in graph_data.get("text", "").split("<br>")
976 if trigger_id == "tput":
978 elif trigger_id == "lat":
980 hdrh_data = graph_data.get("customdata", None)
983 class_name="gy-2 p-0",
985 dbc.CardHeader(hdrh_data.pop("name")),
986 dbc.CardBody(children=[
988 id="hdrh-latency-graph",
989 figure=graph_hdrh_latency(
990 hdrh_data, self.layout
998 class_name="gy-2 p-0",
1000 dbc.CardHeader(children=[
1002 target_id="tput-lat-metadata",
1004 style={"display": "inline-block"}
1009 id="tput-lat-metadata",
1011 children=[dbc.ListGroup(children, flush=True), ]
1017 return metadata, graph, True
1020 Output("download-data", "data"),
1021 State("selected-tests", "data"),
1022 Input("btn-download-data", "n_clicks"),
1023 prevent_initial_call=True
1025 def _download_data(store_sel, n_clicks):
1036 for itm in store_sel:
1037 sel_data = select_trending_data(self.data, itm)
1038 if sel_data is None:
1040 df = pd.concat([df, sel_data], ignore_index=True)
1042 return dcc.send_data_frame(df.to_csv, "trending_data.csv")