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=180)
74 data_spec_file=self._data_spec_file,
76 ).read_trending_ndrpdr(days=180)
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",
418 children="Add Selected",
431 className="d-flex justify-content-center",
433 datetime.utcnow()-timedelta(days=180),
434 max_date_allowed=datetime.utcnow(),
435 initial_visible_month=datetime.utcnow(),
436 start_date=datetime.utcnow() - timedelta(days=180),
437 end_date=datetime.utcnow(),
438 display_format="D MMMM YY"
443 id="row-card-sel-tests",
445 style=self.STYLE_DISABLED,
452 class_name="overflow-auto",
456 style={"max-height": "12em"},
461 id="row-btns-sel-tests",
462 style=self.STYLE_DISABLED,
468 children="Remove Selected",
474 id="btn-sel-remove-all",
475 children="Remove All",
489 def __init__(self, panel: dict) -> None:
497 # Defines also the order of keys
499 "dd-ctrl-phy-value": str(),
500 "dd-ctrl-area-options": list(),
501 "dd-ctrl-area-disabled": True,
502 "dd-ctrl-area-value": str(),
503 "dd-ctrl-test-options": list(),
504 "dd-ctrl-test-disabled": True,
505 "dd-ctrl-test-value": str(),
506 "cl-ctrl-core-options": list(),
507 "cl-ctrl-core-value": list(),
508 "cl-ctrl-core-all-value": list(),
509 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
510 "cl-ctrl-framesize-options": list(),
511 "cl-ctrl-framesize-value": list(),
512 "cl-ctrl-framesize-all-value": list(),
513 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
514 "cl-ctrl-testtype-options": list(),
515 "cl-ctrl-testtype-value": list(),
516 "cl-ctrl-testtype-all-value": list(),
517 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
518 "btn-ctrl-add-disabled": True,
519 "cl-selected-options": list(),
522 self._panel = deepcopy(self._defaults)
524 for key in self._defaults:
525 self._panel[key] = panel[key]
528 def defaults(self) -> dict:
529 return self._defaults
532 def panel(self) -> dict:
535 def set(self, kwargs: dict) -> None:
536 for key, val in kwargs.items():
537 if key in self._panel:
538 self._panel[key] = val
540 raise KeyError(f"The key {key} is not defined.")
542 def get(self, key: str) -> any:
543 return self._panel[key]
545 def values(self) -> tuple:
546 return tuple(self._panel.values())
549 def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
552 options = {v["value"] for v in opt}
554 sel = list(options) if all else list()
556 all = ["all", ] if set(sel) == options else list()
560 def _list_tests(selection: dict) -> list:
561 """Display selected tests with checkboxes
565 {"label": v["id"], "value": v["id"]} for v in selection
570 def callbacks(self, app):
572 def _generate_plotting_arrea(args: tuple) -> tuple:
576 (fig_tput, fig_lat) = args
578 row_fig_tput = self.PLACEHOLDER
579 row_fig_lat = self.PLACEHOLDER
580 row_btn_dwnld = self.PLACEHOLDER
586 id={"type": "graph", "index": "tput"},
592 dcc.Loading(children=[
594 id="btn-download-data",
595 children=["Download Data"]
597 dcc.Download(id="download-data")
604 id={"type": "graph", "index": "lat"},
610 return row_fig_tput, row_fig_lat, row_btn_dwnld
613 Output("control-panel", "data"), # Store
614 Output("selected-tests", "data"), # Store
615 Output("row-graph-tput", "children"),
616 Output("row-graph-lat", "children"),
617 Output("row-btn-download", "children"),
618 Output("row-card-sel-tests", "style"),
619 Output("row-btns-sel-tests", "style"),
620 Output("dd-ctrl-phy", "value"),
621 Output("dd-ctrl-area", "options"),
622 Output("dd-ctrl-area", "disabled"),
623 Output("dd-ctrl-area", "value"),
624 Output("dd-ctrl-test", "options"),
625 Output("dd-ctrl-test", "disabled"),
626 Output("dd-ctrl-test", "value"),
627 Output("cl-ctrl-core", "options"),
628 Output("cl-ctrl-core", "value"),
629 Output("cl-ctrl-core-all", "value"),
630 Output("cl-ctrl-core-all", "options"),
631 Output("cl-ctrl-framesize", "options"),
632 Output("cl-ctrl-framesize", "value"),
633 Output("cl-ctrl-framesize-all", "value"),
634 Output("cl-ctrl-framesize-all", "options"),
635 Output("cl-ctrl-testtype", "options"),
636 Output("cl-ctrl-testtype", "value"),
637 Output("cl-ctrl-testtype-all", "value"),
638 Output("cl-ctrl-testtype-all", "options"),
639 Output("btn-ctrl-add", "disabled"),
640 Output("cl-selected", "options"), # User selection
641 State("control-panel", "data"), # Store
642 State("selected-tests", "data"), # Store
643 State("cl-selected", "value"), # User selection
644 Input("dd-ctrl-phy", "value"),
645 Input("dd-ctrl-area", "value"),
646 Input("dd-ctrl-test", "value"),
647 Input("cl-ctrl-core", "value"),
648 Input("cl-ctrl-core-all", "value"),
649 Input("cl-ctrl-framesize", "value"),
650 Input("cl-ctrl-framesize-all", "value"),
651 Input("cl-ctrl-testtype", "value"),
652 Input("cl-ctrl-testtype-all", "value"),
653 Input("btn-ctrl-add", "n_clicks"),
654 Input("dpr-period", "start_date"),
655 Input("dpr-period", "end_date"),
656 Input("btn-sel-remove", "n_clicks"),
657 Input("btn-sel-remove-all", "n_clicks"),
659 def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
660 dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
661 cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
662 cl_testtype: list, cl_testtype_all: list, btn_add: int,
663 d_start: str, d_end: str, btn_remove: int,
664 btn_remove_all: int) -> tuple:
668 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
670 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
672 row_fig_tput = no_update
673 row_fig_lat = no_update
674 row_btn_dwnld = no_update
675 row_card_sel_tests = no_update
676 row_btns_sel_tests = no_update
678 ctrl_panel = self.ControlPanel(cp_data)
680 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
682 if trigger_id == "dd-ctrl-phy":
685 {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
686 for v in [v for v in self.spec_tbs[dd_phy].keys()]
693 "dd-ctrl-phy-value": dd_phy,
694 "dd-ctrl-area-value": str(),
695 "dd-ctrl-area-options": options,
696 "dd-ctrl-area-disabled": disabled,
697 "dd-ctrl-test-options": list(),
698 "dd-ctrl-test-disabled": True,
699 "cl-ctrl-core-options": list(),
700 "cl-ctrl-core-value": list(),
701 "cl-ctrl-core-all-value": list(),
702 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
703 "cl-ctrl-framesize-options": list(),
704 "cl-ctrl-framesize-value": list(),
705 "cl-ctrl-framesize-all-value": list(),
706 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
707 "cl-ctrl-testtype-options": list(),
708 "cl-ctrl-testtype-value": list(),
709 "cl-ctrl-testtype-all-value": list(),
710 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
711 "btn-ctrl-add-disabled": True,
713 elif trigger_id == "dd-ctrl-area":
715 phy = ctrl_panel.get("dd-ctrl-phy-value")
717 {"label": v, "value": v}
718 for v in self.spec_tbs[phy][dd_area]["test"]
725 "dd-ctrl-area-value": dd_area,
726 "dd-ctrl-test-value": str(),
727 "dd-ctrl-test-options": options,
728 "dd-ctrl-test-disabled": disabled,
729 "cl-ctrl-core-options": list(),
730 "cl-ctrl-core-value": list(),
731 "cl-ctrl-core-all-value": list(),
732 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
733 "cl-ctrl-framesize-options": list(),
734 "cl-ctrl-framesize-value": list(),
735 "cl-ctrl-framesize-all-value": list(),
736 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
737 "cl-ctrl-testtype-options": list(),
738 "cl-ctrl-testtype-value": list(),
739 "cl-ctrl-testtype-all-value": list(),
740 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
741 "btn-ctrl-add-disabled": True,
743 elif trigger_id == "dd-ctrl-test":
745 framesize_opts = list()
746 testtype_opts = list()
747 phy = ctrl_panel.get("dd-ctrl-phy-value")
748 area = ctrl_panel.get("dd-ctrl-area-value")
749 if phy and area and dd_test:
751 {"label": v, "value": v}
752 for v in self.spec_tbs[phy][area]["core"]
755 {"label": v, "value": v}
756 for v in self.spec_tbs[phy][area]["frame-size"]
759 {"label": v, "value": v}
760 for v in self.spec_tbs[phy][area]["test-type"]
763 "dd-ctrl-test-value": dd_test,
764 "cl-ctrl-core-options": core_opts,
765 "cl-ctrl-core-value": list(),
766 "cl-ctrl-core-all-value": list(),
767 "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
768 "cl-ctrl-framesize-options": framesize_opts,
769 "cl-ctrl-framesize-value": list(),
770 "cl-ctrl-framesize-all-value": list(),
771 "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
772 "cl-ctrl-testtype-options": testtype_opts,
773 "cl-ctrl-testtype-value": list(),
774 "cl-ctrl-testtype-all-value": list(),
775 "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
776 "btn-ctrl-add-disabled": False,
778 elif trigger_id == "cl-ctrl-core":
779 val_sel, val_all = self._sync_checklists(
780 opt=ctrl_panel.get("cl-ctrl-core-options"),
786 "cl-ctrl-core-value": val_sel,
787 "cl-ctrl-core-all-value": val_all,
789 elif trigger_id == "cl-ctrl-core-all":
790 val_sel, val_all = self._sync_checklists(
791 opt = ctrl_panel.get("cl-ctrl-core-options"),
797 "cl-ctrl-core-value": val_sel,
798 "cl-ctrl-core-all-value": val_all,
800 elif trigger_id == "cl-ctrl-framesize":
801 val_sel, val_all = self._sync_checklists(
802 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
808 "cl-ctrl-framesize-value": val_sel,
809 "cl-ctrl-framesize-all-value": val_all,
811 elif trigger_id == "cl-ctrl-framesize-all":
812 val_sel, val_all = self._sync_checklists(
813 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
815 all=cl_framesize_all,
819 "cl-ctrl-framesize-value": val_sel,
820 "cl-ctrl-framesize-all-value": val_all,
822 elif trigger_id == "cl-ctrl-testtype":
823 val_sel, val_all = self._sync_checklists(
824 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
830 "cl-ctrl-testtype-value": val_sel,
831 "cl-ctrl-testtype-all-value": val_all,
833 elif trigger_id == "cl-ctrl-testtype-all":
834 val_sel, val_all = self._sync_checklists(
835 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
841 "cl-ctrl-testtype-value": val_sel,
842 "cl-ctrl-testtype-all-value": val_all,
844 elif trigger_id == "btn-ctrl-add":
846 phy = ctrl_panel.get("dd-ctrl-phy-value")
847 area = ctrl_panel.get("dd-ctrl-area-value")
848 test = ctrl_panel.get("dd-ctrl-test-value")
849 cores = ctrl_panel.get("cl-ctrl-core-value")
850 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
851 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
852 # Add selected test to the list of tests in store:
853 if phy and area and test and cores and framesizes and testtypes:
854 if store_sel is None:
857 for framesize in framesizes:
858 for ttype in testtypes:
860 f"{phy.replace('af_xdp', 'af-xdp')}-"
862 f"{framesize.lower()}-"
867 if tid not in [itm["id"] for itm in store_sel]:
873 "framesize": framesize.lower(),
874 "core": core.lower(),
875 "testtype": ttype.lower()
877 row_card_sel_tests = self.STYLE_ENABLED
878 row_btns_sel_tests = self.STYLE_ENABLED
879 ctrl_panel.set(ctrl_panel.defaults)
881 "cl-selected-options": self._list_tests(store_sel)
883 row_fig_tput, row_fig_lat, row_btn_dwnld = \
884 _generate_plotting_arrea(
886 self.data, store_sel, self.layout, d_start, d_end
889 elif trigger_id == "dpr-period":
890 row_fig_tput, row_fig_lat, row_btn_dwnld = \
891 _generate_plotting_arrea(
893 self.data, store_sel, self.layout, d_start, d_end
896 elif trigger_id == "btn-sel-remove-all":
898 row_fig_tput = self.PLACEHOLDER
899 row_fig_lat = self.PLACEHOLDER
900 row_btn_dwnld = self.PLACEHOLDER
901 row_card_sel_tests = self.STYLE_DISABLED
902 row_btns_sel_tests = self.STYLE_DISABLED
905 "cl-selected-options": list()
907 elif trigger_id == "btn-sel-remove":
910 new_store_sel = list()
911 for item in store_sel:
912 if item["id"] not in list_sel:
913 new_store_sel.append(item)
914 store_sel = new_store_sel
916 row_fig_tput, row_fig_lat, row_btn_dwnld = \
917 _generate_plotting_arrea(
919 self.data, store_sel, self.layout, d_start, d_end
923 "cl-selected-options": self._list_tests(store_sel)
926 row_fig_tput = self.PLACEHOLDER
927 row_fig_lat = self.PLACEHOLDER
928 row_btn_dwnld = self.PLACEHOLDER
929 row_card_sel_tests = self.STYLE_DISABLED
930 row_btns_sel_tests = self.STYLE_DISABLED
933 "cl-selected-options": list()
937 ctrl_panel.panel, store_sel,
938 row_fig_tput, row_fig_lat, row_btn_dwnld,
939 row_card_sel_tests, row_btns_sel_tests
941 ret_val.extend(ctrl_panel.values())
945 Output("metadata-tput-lat", "children"),
946 Output("metadata-hdrh-graph", "children"),
947 Output("offcanvas-metadata", "is_open"),
948 Input({"type": "graph", "index": ALL}, "clickData"),
949 prevent_initial_call=True
951 def _show_metadata_from_graphs(graph_data: dict) -> tuple:
956 callback_context.triggered[0]["prop_id"].split(".")[0]
958 idx = 0 if trigger_id == "tput" else 1
959 graph_data = graph_data[idx]["points"][0]
960 except (JSONDecodeError, IndexError, KeyError, ValueError,
969 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
970 ) for x in graph_data.get("text", "").split("<br>")
972 if trigger_id == "tput":
974 elif trigger_id == "lat":
976 hdrh_data = graph_data.get("customdata", None)
979 class_name="gy-2 p-0",
981 dbc.CardHeader(hdrh_data.pop("name")),
982 dbc.CardBody(children=[
984 id="hdrh-latency-graph",
985 figure=graph_hdrh_latency(
986 hdrh_data, self.layout
994 class_name="gy-2 p-0",
996 dbc.CardHeader(children=[
998 target_id="tput-lat-metadata",
1000 style={"display": "inline-block"}
1005 id="tput-lat-metadata",
1007 children=[dbc.ListGroup(children, flush=True), ]
1013 return metadata, graph, True
1016 Output("download-data", "data"),
1017 State("selected-tests", "data"),
1018 Input("btn-download-data", "n_clicks"),
1019 prevent_initial_call=True
1021 def _download_data(store_sel, n_clicks):
1032 for itm in store_sel:
1033 sel_data = select_trending_data(self.data, itm)
1034 if sel_data is None:
1036 df = pd.concat([df, sel_data], ignore_index=True)
1038 return dcc.send_data_frame(df.to_csv, "trending_data.csv")