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
20 from flask import Flask
23 from dash import callback_context, no_update, ALL
24 from dash import Input, Output, State
25 from dash.exceptions import PreventUpdate
26 from yaml import load, FullLoader, YAMLError
27 from datetime import datetime, timedelta
28 from copy import deepcopy
29 from json import loads, JSONDecodeError
31 from ..data.data import Data
32 from .graphs import graph_trending, graph_hdrh_latency, \
40 STYLE_DISABLED = {"display": "none"}
41 STYLE_ENABLED = {"display": "inherit"}
54 PLACEHOLDER = html.Nobr("")
56 def __init__(self, app: Flask, html_layout_file: str, spec_file: str,
57 graph_layout_file: str, data_spec_file: str,
58 time_period: str=None) -> None:
64 self._html_layout_file = html_layout_file
65 self._spec_file = spec_file
66 self._graph_layout_file = graph_layout_file
67 self._data_spec_file = data_spec_file
68 self._time_period = time_period
72 data_spec_file=self._data_spec_file,
74 ).read_trending_mrr(days=self._time_period)
77 data_spec_file=self._data_spec_file,
79 ).read_trending_ndrpdr(days=self._time_period)
81 self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
84 (datetime.utcnow() - self._data["start_time"].min()).days
85 if self._time_period > data_time_period:
86 self._time_period = data_time_period
89 self._html_layout = ""
91 self._graph_layout = None
94 with open(self._html_layout_file, "r") as file_read:
95 self._html_layout = file_read.read()
96 except IOError as err:
98 f"Not possible to open the file {self._html_layout_file}\n{err}"
102 with open(self._spec_file, "r") as file_read:
103 self._spec_tbs = load(file_read, Loader=FullLoader)
104 except IOError as err:
106 f"Not possible to open the file {self._spec_file,}\n{err}"
108 except YAMLError as err:
110 f"An error occurred while parsing the specification file "
111 f"{self._spec_file,}\n"
116 with open(self._graph_layout_file, "r") as file_read:
117 self._graph_layout = load(file_read, Loader=FullLoader)
118 except IOError as err:
120 f"Not possible to open the file {self._graph_layout_file}\n"
123 except YAMLError as err:
125 f"An error occurred while parsing the specification file "
126 f"{self._graph_layout_file}\n"
131 if self._app is not None and hasattr(self, 'callbacks'):
132 self.callbacks(self._app)
135 def html_layout(self):
136 return self._html_layout
140 return self._spec_tbs
148 return self._graph_layout
151 def time_period(self):
152 return self._time_period
154 def add_content(self):
157 if self.html_layout and self.spec_tbs:
171 id="offcanvas-metadata",
172 title="Throughput And Latency",
176 dbc.Row(id="metadata-tput-lat"),
177 dbc.Row(id="metadata-hdrh-graph"),
191 self._add_ctrl_col(),
192 self._add_plotting_col(),
210 def _add_navbar(self):
211 """Add nav element with navigation panel. It is placed on the top.
213 return dbc.NavbarSimple(
214 id="navbarsimple-main",
218 "Continuous Performance Trending",
227 brand_external_link=True,
232 def _add_ctrl_col(self) -> dbc.Col:
233 """Add column with controls. It is placed on the left side.
238 self._add_ctrl_panel(),
242 def _add_plotting_col(self) -> dbc.Col:
243 """Add column with plots and tables. It is placed on the right side.
246 id="col-plotting-area",
248 dbc.Row( # Throughput
250 class_name="g-0 p-2",
257 class_name="g-0 p-2",
263 id="row-btn-download",
264 class_name="g-0 p-2",
273 def _add_ctrl_panel(self) -> dbc.Row:
278 class_name="g-0 p-2",
285 dbc.InputGroupText("Infra"),
289 "Select a Physical Test Bed "
293 {"label": k, "value": k} \
294 for k in self.spec_tbs.keys()
308 dbc.InputGroupText("Area"),
311 placeholder="Select an Area...",
325 dbc.InputGroupText("Test"),
328 placeholder="Select a Test...",
348 id="cl-ctrl-core-all",
349 options=self.CL_ALL_DISABLED,
368 id="row-ctrl-framesize",
378 id="cl-ctrl-framesize-all",
379 options=self.CL_ALL_DISABLED,
389 id="cl-ctrl-framesize",
398 id="row-ctrl-testtype",
408 id="cl-ctrl-testtype-all",
409 options=self.CL_ALL_DISABLED,
419 id="cl-ctrl-testtype",
428 class_name="gy-1 p-0",
434 children="Add Selected",
448 className="d-flex justify-content-center",
450 datetime.utcnow() - timedelta(
451 days=self.time_period),
452 max_date_allowed=datetime.utcnow(),
453 initial_visible_month=datetime.utcnow(),
455 datetime.utcnow() - timedelta(
456 days=self.time_period),
457 end_date=datetime.utcnow(),
458 display_format="D MMMM YY"
463 id="row-card-sel-tests",
465 style=self.STYLE_DISABLED,
472 class_name="overflow-auto",
476 style={"max-height": "12em"},
481 id="row-btns-sel-tests",
482 style=self.STYLE_DISABLED,
489 children="Remove Selected",
490 class_name="w-100 me-1",
495 id="btn-sel-remove-all",
496 children="Remove All",
497 class_name="w-100 me-1",
510 def __init__(self, panel: dict) -> None:
518 # Defines also the order of keys
520 "dd-ctrl-phy-value": str(),
521 "dd-ctrl-area-options": list(),
522 "dd-ctrl-area-disabled": True,
523 "dd-ctrl-area-value": str(),
524 "dd-ctrl-test-options": list(),
525 "dd-ctrl-test-disabled": True,
526 "dd-ctrl-test-value": str(),
527 "cl-ctrl-core-options": list(),
528 "cl-ctrl-core-value": list(),
529 "cl-ctrl-core-all-value": list(),
530 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
531 "cl-ctrl-framesize-options": list(),
532 "cl-ctrl-framesize-value": list(),
533 "cl-ctrl-framesize-all-value": list(),
534 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
535 "cl-ctrl-testtype-options": list(),
536 "cl-ctrl-testtype-value": list(),
537 "cl-ctrl-testtype-all-value": list(),
538 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
539 "btn-ctrl-add-disabled": True,
540 "cl-selected-options": list(),
543 self._panel = deepcopy(self._defaults)
545 for key in self._defaults:
546 self._panel[key] = panel[key]
549 def defaults(self) -> dict:
550 return self._defaults
553 def panel(self) -> dict:
556 def set(self, kwargs: dict) -> None:
557 for key, val in kwargs.items():
558 if key in self._panel:
559 self._panel[key] = val
561 raise KeyError(f"The key {key} is not defined.")
563 def get(self, key: str) -> any:
564 return self._panel[key]
566 def values(self) -> tuple:
567 return tuple(self._panel.values())
570 def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
573 options = {v["value"] for v in opt}
575 sel = list(options) if all else list()
577 all = ["all", ] if set(sel) == options else list()
581 def _list_tests(selection: dict) -> list:
582 """Display selected tests with checkboxes
586 {"label": v["id"], "value": v["id"]} for v in selection
591 def callbacks(self, app):
593 def _generate_plotting_arrea(args: tuple) -> tuple:
597 (fig_tput, fig_lat) = args
599 row_fig_tput = self.PLACEHOLDER
600 row_fig_lat = self.PLACEHOLDER
601 row_btn_dwnld = self.PLACEHOLDER
607 id={"type": "graph", "index": "tput"},
613 dcc.Loading(children=[
615 id="btn-download-data",
616 children=["Download Data"],
620 dcc.Download(id="download-data")
627 id={"type": "graph", "index": "lat"},
633 return row_fig_tput, row_fig_lat, row_btn_dwnld
636 Output("control-panel", "data"), # Store
637 Output("selected-tests", "data"), # Store
638 Output("row-graph-tput", "children"),
639 Output("row-graph-lat", "children"),
640 Output("row-btn-download", "children"),
641 Output("row-card-sel-tests", "style"),
642 Output("row-btns-sel-tests", "style"),
643 Output("dd-ctrl-phy", "value"),
644 Output("dd-ctrl-area", "options"),
645 Output("dd-ctrl-area", "disabled"),
646 Output("dd-ctrl-area", "value"),
647 Output("dd-ctrl-test", "options"),
648 Output("dd-ctrl-test", "disabled"),
649 Output("dd-ctrl-test", "value"),
650 Output("cl-ctrl-core", "options"),
651 Output("cl-ctrl-core", "value"),
652 Output("cl-ctrl-core-all", "value"),
653 Output("cl-ctrl-core-all", "options"),
654 Output("cl-ctrl-framesize", "options"),
655 Output("cl-ctrl-framesize", "value"),
656 Output("cl-ctrl-framesize-all", "value"),
657 Output("cl-ctrl-framesize-all", "options"),
658 Output("cl-ctrl-testtype", "options"),
659 Output("cl-ctrl-testtype", "value"),
660 Output("cl-ctrl-testtype-all", "value"),
661 Output("cl-ctrl-testtype-all", "options"),
662 Output("btn-ctrl-add", "disabled"),
663 Output("cl-selected", "options"), # User selection
664 State("control-panel", "data"), # Store
665 State("selected-tests", "data"), # Store
666 State("cl-selected", "value"), # User selection
667 Input("dd-ctrl-phy", "value"),
668 Input("dd-ctrl-area", "value"),
669 Input("dd-ctrl-test", "value"),
670 Input("cl-ctrl-core", "value"),
671 Input("cl-ctrl-core-all", "value"),
672 Input("cl-ctrl-framesize", "value"),
673 Input("cl-ctrl-framesize-all", "value"),
674 Input("cl-ctrl-testtype", "value"),
675 Input("cl-ctrl-testtype-all", "value"),
676 Input("btn-ctrl-add", "n_clicks"),
677 Input("dpr-period", "start_date"),
678 Input("dpr-period", "end_date"),
679 Input("btn-sel-remove", "n_clicks"),
680 Input("btn-sel-remove-all", "n_clicks"),
682 def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
683 dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
684 cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
685 cl_testtype: list, cl_testtype_all: list, btn_add: int,
686 d_start: str, d_end: str, btn_remove: int,
687 btn_remove_all: int) -> tuple:
691 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
693 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
695 row_fig_tput = no_update
696 row_fig_lat = no_update
697 row_btn_dwnld = no_update
698 row_card_sel_tests = no_update
699 row_btns_sel_tests = no_update
701 ctrl_panel = self.ControlPanel(cp_data)
703 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
705 if trigger_id == "dd-ctrl-phy":
708 {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
709 for v in [v for v in self.spec_tbs[dd_phy].keys()]
716 "dd-ctrl-phy-value": dd_phy,
717 "dd-ctrl-area-value": str(),
718 "dd-ctrl-area-options": options,
719 "dd-ctrl-area-disabled": disabled,
720 "dd-ctrl-test-options": list(),
721 "dd-ctrl-test-disabled": True,
722 "cl-ctrl-core-options": list(),
723 "cl-ctrl-core-value": list(),
724 "cl-ctrl-core-all-value": list(),
725 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
726 "cl-ctrl-framesize-options": list(),
727 "cl-ctrl-framesize-value": list(),
728 "cl-ctrl-framesize-all-value": list(),
729 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
730 "cl-ctrl-testtype-options": list(),
731 "cl-ctrl-testtype-value": list(),
732 "cl-ctrl-testtype-all-value": list(),
733 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
734 "btn-ctrl-add-disabled": True,
736 elif trigger_id == "dd-ctrl-area":
738 phy = ctrl_panel.get("dd-ctrl-phy-value")
740 {"label": v, "value": v}
741 for v in self.spec_tbs[phy][dd_area]["test"]
748 "dd-ctrl-area-value": dd_area,
749 "dd-ctrl-test-value": str(),
750 "dd-ctrl-test-options": options,
751 "dd-ctrl-test-disabled": disabled,
752 "cl-ctrl-core-options": list(),
753 "cl-ctrl-core-value": list(),
754 "cl-ctrl-core-all-value": list(),
755 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
756 "cl-ctrl-framesize-options": list(),
757 "cl-ctrl-framesize-value": list(),
758 "cl-ctrl-framesize-all-value": list(),
759 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
760 "cl-ctrl-testtype-options": list(),
761 "cl-ctrl-testtype-value": list(),
762 "cl-ctrl-testtype-all-value": list(),
763 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
764 "btn-ctrl-add-disabled": True,
766 elif trigger_id == "dd-ctrl-test":
768 framesize_opts = list()
769 testtype_opts = list()
770 phy = ctrl_panel.get("dd-ctrl-phy-value")
771 area = ctrl_panel.get("dd-ctrl-area-value")
772 if phy and area and dd_test:
774 {"label": v, "value": v}
775 for v in self.spec_tbs[phy][area]["core"]
778 {"label": v, "value": v}
779 for v in self.spec_tbs[phy][area]["frame-size"]
782 {"label": v, "value": v}
783 for v in self.spec_tbs[phy][area]["test-type"]
786 "dd-ctrl-test-value": dd_test,
787 "cl-ctrl-core-options": core_opts,
788 "cl-ctrl-core-value": list(),
789 "cl-ctrl-core-all-value": list(),
790 "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
791 "cl-ctrl-framesize-options": framesize_opts,
792 "cl-ctrl-framesize-value": list(),
793 "cl-ctrl-framesize-all-value": list(),
794 "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
795 "cl-ctrl-testtype-options": testtype_opts,
796 "cl-ctrl-testtype-value": list(),
797 "cl-ctrl-testtype-all-value": list(),
798 "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
799 "btn-ctrl-add-disabled": False,
801 elif trigger_id == "cl-ctrl-core":
802 val_sel, val_all = self._sync_checklists(
803 opt=ctrl_panel.get("cl-ctrl-core-options"),
809 "cl-ctrl-core-value": val_sel,
810 "cl-ctrl-core-all-value": val_all,
812 elif trigger_id == "cl-ctrl-core-all":
813 val_sel, val_all = self._sync_checklists(
814 opt = ctrl_panel.get("cl-ctrl-core-options"),
820 "cl-ctrl-core-value": val_sel,
821 "cl-ctrl-core-all-value": val_all,
823 elif trigger_id == "cl-ctrl-framesize":
824 val_sel, val_all = self._sync_checklists(
825 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
831 "cl-ctrl-framesize-value": val_sel,
832 "cl-ctrl-framesize-all-value": val_all,
834 elif trigger_id == "cl-ctrl-framesize-all":
835 val_sel, val_all = self._sync_checklists(
836 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
838 all=cl_framesize_all,
842 "cl-ctrl-framesize-value": val_sel,
843 "cl-ctrl-framesize-all-value": val_all,
845 elif trigger_id == "cl-ctrl-testtype":
846 val_sel, val_all = self._sync_checklists(
847 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
853 "cl-ctrl-testtype-value": val_sel,
854 "cl-ctrl-testtype-all-value": val_all,
856 elif trigger_id == "cl-ctrl-testtype-all":
857 val_sel, val_all = self._sync_checklists(
858 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
864 "cl-ctrl-testtype-value": val_sel,
865 "cl-ctrl-testtype-all-value": val_all,
867 elif trigger_id == "btn-ctrl-add":
869 phy = ctrl_panel.get("dd-ctrl-phy-value")
870 area = ctrl_panel.get("dd-ctrl-area-value")
871 test = ctrl_panel.get("dd-ctrl-test-value")
872 cores = ctrl_panel.get("cl-ctrl-core-value")
873 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
874 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
875 # Add selected test to the list of tests in store:
876 if phy and area and test and cores and framesizes and testtypes:
877 if store_sel is None:
880 for framesize in framesizes:
881 for ttype in testtypes:
883 f"{phy.replace('af_xdp', 'af-xdp')}-"
885 f"{framesize.lower()}-"
890 if tid not in [itm["id"] for itm in store_sel]:
896 "framesize": framesize.lower(),
897 "core": core.lower(),
898 "testtype": ttype.lower()
900 row_card_sel_tests = self.STYLE_ENABLED
901 row_btns_sel_tests = self.STYLE_ENABLED
902 ctrl_panel.set(ctrl_panel.defaults)
904 "cl-selected-options": self._list_tests(store_sel)
906 row_fig_tput, row_fig_lat, row_btn_dwnld = \
907 _generate_plotting_arrea(
909 self.data, store_sel, self.layout, d_start, d_end
912 elif trigger_id == "dpr-period":
913 row_fig_tput, row_fig_lat, row_btn_dwnld = \
914 _generate_plotting_arrea(
916 self.data, store_sel, self.layout, d_start, d_end
919 elif trigger_id == "btn-sel-remove-all":
921 row_fig_tput = self.PLACEHOLDER
922 row_fig_lat = self.PLACEHOLDER
923 row_btn_dwnld = self.PLACEHOLDER
924 row_card_sel_tests = self.STYLE_DISABLED
925 row_btns_sel_tests = self.STYLE_DISABLED
928 "cl-selected-options": list()
930 elif trigger_id == "btn-sel-remove":
933 new_store_sel = list()
934 for item in store_sel:
935 if item["id"] not in list_sel:
936 new_store_sel.append(item)
937 store_sel = new_store_sel
939 row_fig_tput, row_fig_lat, row_btn_dwnld = \
940 _generate_plotting_arrea(
942 self.data, store_sel, self.layout, d_start, d_end
946 "cl-selected-options": self._list_tests(store_sel)
949 row_fig_tput = self.PLACEHOLDER
950 row_fig_lat = self.PLACEHOLDER
951 row_btn_dwnld = self.PLACEHOLDER
952 row_card_sel_tests = self.STYLE_DISABLED
953 row_btns_sel_tests = self.STYLE_DISABLED
956 "cl-selected-options": list()
960 ctrl_panel.panel, store_sel,
961 row_fig_tput, row_fig_lat, row_btn_dwnld,
962 row_card_sel_tests, row_btns_sel_tests
964 ret_val.extend(ctrl_panel.values())
968 Output("metadata-tput-lat", "children"),
969 Output("metadata-hdrh-graph", "children"),
970 Output("offcanvas-metadata", "is_open"),
971 Input({"type": "graph", "index": ALL}, "clickData"),
972 prevent_initial_call=True
974 def _show_metadata_from_graphs(graph_data: dict) -> tuple:
979 callback_context.triggered[0]["prop_id"].split(".")[0]
981 idx = 0 if trigger_id == "tput" else 1
982 graph_data = graph_data[idx]["points"][0]
983 except (JSONDecodeError, IndexError, KeyError, ValueError,
992 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
993 ) for x in graph_data.get("text", "").split("<br>")
995 if trigger_id == "tput":
997 elif trigger_id == "lat":
999 hdrh_data = graph_data.get("customdata", None)
1002 class_name="gy-2 p-0",
1004 dbc.CardHeader(hdrh_data.pop("name")),
1005 dbc.CardBody(children=[
1007 id="hdrh-latency-graph",
1008 figure=graph_hdrh_latency(
1009 hdrh_data, self.layout
1017 class_name="gy-2 p-0",
1019 dbc.CardHeader(children=[
1021 target_id="tput-lat-metadata",
1023 style={"display": "inline-block"}
1028 id="tput-lat-metadata",
1030 children=[dbc.ListGroup(children, flush=True), ]
1036 return metadata, graph, True
1039 Output("download-data", "data"),
1040 State("selected-tests", "data"),
1041 Input("btn-download-data", "n_clicks"),
1042 prevent_initial_call=True
1044 def _download_data(store_sel, n_clicks):
1055 for itm in store_sel:
1056 sel_data = select_trending_data(self.data, itm)
1057 if sel_data is None:
1059 df = pd.concat([df, sel_data], ignore_index=True)
1061 return dcc.send_data_frame(df.to_csv, "trending_data.csv")