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"),
167 class_name="g-0 p-2",
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(),
223 self._add_ctrl_shown()
227 def _add_plotting_col(self) -> dbc.Col:
228 """Add column with plots and tables. It is placed on the right side.
231 id="col-plotting-area",
233 dbc.Row( # Throughput
235 class_name="g-0 p-2",
238 dcc.Graph(id="graph-tput")
244 class_name="g-0 p-2",
247 dcc.Graph(id="graph-latency")
255 dcc.Loading(children=[
257 id="btn-download-data",
258 children=["Download Data"]
260 dcc.Download(id="download-data")
268 def _add_ctrl_panel(self) -> dbc.Row:
275 dbc.Label("Physical Test Bed Topology, NIC and Driver"),
279 placeholder="Select a Physical Test Bed Topology...",
281 {"label": k, "value": k} for k in self.spec_tbs.keys()
289 placeholder="Select an Area...",
297 placeholder="Select a Test...",
305 dbc.Label("Number of Cores"),
308 id="cl-ctrl-core-all",
309 options=self.CL_ALL_DISABLED,
324 id="row-ctrl-framesize",
327 dbc.Label("Frame Size"),
330 id="cl-ctrl-framesize-all",
331 options=self.CL_ALL_DISABLED,
338 id="cl-ctrl-framesize",
346 id="row-ctrl-testtype",
349 dbc.Label("Test Type"),
352 id="cl-ctrl-testtype-all",
353 options=self.CL_ALL_DISABLED,
360 id="cl-ctrl-testtype",
383 datetime.utcnow()-timedelta(days=180),
384 max_date_allowed=datetime.utcnow(),
385 initial_visible_month=datetime.utcnow(),
386 start_date=datetime.utcnow() - timedelta(days=180),
387 end_date=datetime.utcnow(),
388 display_format="D MMMM YY"
395 def _add_ctrl_shown(self) -> dbc.Row:
405 dbc.Label("Selected tests"),
419 id="btn-sel-remove-all",
420 children="Remove All",
426 children="Remove Selected",
431 id="btn-sel-display",
446 def __init__(self, panel: dict) -> None:
454 # Defines also the order of keys
456 "dd-ctrl-phy-value": str(),
457 "dd-ctrl-area-options": list(),
458 "dd-ctrl-area-disabled": True,
459 "dd-ctrl-area-value": str(),
460 "dd-ctrl-test-options": list(),
461 "dd-ctrl-test-disabled": True,
462 "dd-ctrl-test-value": str(),
463 "cl-ctrl-core-options": list(),
464 "cl-ctrl-core-value": list(),
465 "cl-ctrl-core-all-value": list(),
466 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
467 "cl-ctrl-framesize-options": list(),
468 "cl-ctrl-framesize-value": list(),
469 "cl-ctrl-framesize-all-value": list(),
470 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
471 "cl-ctrl-testtype-options": list(),
472 "cl-ctrl-testtype-value": list(),
473 "cl-ctrl-testtype-all-value": list(),
474 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
475 "btn-ctrl-add-disabled": True,
476 "cl-selected-options": list(),
479 self._panel = deepcopy(self._defaults)
481 for key in self._defaults:
482 self._panel[key] = panel[key]
485 def defaults(self) -> dict:
486 return self._defaults
489 def panel(self) -> dict:
492 def set(self, kwargs: dict) -> None:
493 for key, val in kwargs.items():
494 if key in self._panel:
495 self._panel[key] = val
497 raise KeyError(f"The key {key} is not defined.")
499 def get(self, key: str) -> any:
500 return self._panel[key]
502 def values(self) -> tuple:
503 return tuple(self._panel.values())
506 def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
509 options = {v["value"] for v in opt}
511 sel = list(options) if all else list()
513 all = ["all", ] if set(sel) == options else list()
517 def _list_tests(selection: dict) -> list:
518 """Display selected tests with checkboxes
522 {"label": v["id"], "value": v["id"]} for v in selection
527 def callbacks(self, app):
530 Output("control-panel", "data"), # Store
531 Output("selected-tests", "data"), # Store
532 Output("graph-tput", "figure"),
533 Output("graph-latency", "figure"),
534 Output("dd-ctrl-phy", "value"),
535 Output("dd-ctrl-area", "options"),
536 Output("dd-ctrl-area", "disabled"),
537 Output("dd-ctrl-area", "value"),
538 Output("dd-ctrl-test", "options"),
539 Output("dd-ctrl-test", "disabled"),
540 Output("dd-ctrl-test", "value"),
541 Output("cl-ctrl-core", "options"),
542 Output("cl-ctrl-core", "value"),
543 Output("cl-ctrl-core-all", "value"),
544 Output("cl-ctrl-core-all", "options"),
545 Output("cl-ctrl-framesize", "options"),
546 Output("cl-ctrl-framesize", "value"),
547 Output("cl-ctrl-framesize-all", "value"),
548 Output("cl-ctrl-framesize-all", "options"),
549 Output("cl-ctrl-testtype", "options"),
550 Output("cl-ctrl-testtype", "value"),
551 Output("cl-ctrl-testtype-all", "value"),
552 Output("cl-ctrl-testtype-all", "options"),
553 Output("btn-ctrl-add", "disabled"),
554 Output("cl-selected", "options"), # User selection
555 State("control-panel", "data"), # Store
556 State("selected-tests", "data"), # Store
557 State("cl-selected", "value"), # User selection
558 Input("dd-ctrl-phy", "value"),
559 Input("dd-ctrl-area", "value"),
560 Input("dd-ctrl-test", "value"),
561 Input("cl-ctrl-core", "value"),
562 Input("cl-ctrl-core-all", "value"),
563 Input("cl-ctrl-framesize", "value"),
564 Input("cl-ctrl-framesize-all", "value"),
565 Input("cl-ctrl-testtype", "value"),
566 Input("cl-ctrl-testtype-all", "value"),
567 Input("btn-ctrl-add", "n_clicks"),
568 Input("dpr-period", "start_date"),
569 Input("dpr-period", "end_date"),
570 Input("btn-sel-display", "n_clicks"),
571 Input("btn-sel-remove", "n_clicks"),
572 Input("btn-sel-remove-all", "n_clicks"),
574 def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
575 dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
576 cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
577 cl_testtype: list, cl_testtype_all: list, btn_add: int,
578 d_start: str, d_end: str, btn_display: int, btn_remove: int,
579 btn_remove_all: int) -> tuple:
583 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
585 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
590 ctrl_panel = self.ControlPanel(cp_data)
592 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
594 if trigger_id == "dd-ctrl-phy":
597 {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
598 for v in [v for v in self.spec_tbs[dd_phy].keys()]
605 "dd-ctrl-phy-value": dd_phy,
606 "dd-ctrl-area-value": str(),
607 "dd-ctrl-area-options": options,
608 "dd-ctrl-area-disabled": disabled,
609 "dd-ctrl-test-options": list(),
610 "dd-ctrl-test-disabled": True,
611 "cl-ctrl-core-options": list(),
612 "cl-ctrl-core-value": list(),
613 "cl-ctrl-core-all-value": list(),
614 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
615 "cl-ctrl-framesize-options": list(),
616 "cl-ctrl-framesize-value": list(),
617 "cl-ctrl-framesize-all-value": list(),
618 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
619 "cl-ctrl-testtype-options": list(),
620 "cl-ctrl-testtype-value": list(),
621 "cl-ctrl-testtype-all-value": list(),
622 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
623 "btn-ctrl-add-disabled": True,
625 elif trigger_id == "dd-ctrl-area":
627 phy = ctrl_panel.get("dd-ctrl-phy-value")
629 {"label": v, "value": v}
630 for v in self.spec_tbs[phy][dd_area]["test"]
637 "dd-ctrl-area-value": dd_area,
638 "dd-ctrl-test-value": str(),
639 "dd-ctrl-test-options": options,
640 "dd-ctrl-test-disabled": disabled,
641 "cl-ctrl-core-options": list(),
642 "cl-ctrl-core-value": list(),
643 "cl-ctrl-core-all-value": list(),
644 "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
645 "cl-ctrl-framesize-options": list(),
646 "cl-ctrl-framesize-value": list(),
647 "cl-ctrl-framesize-all-value": list(),
648 "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
649 "cl-ctrl-testtype-options": list(),
650 "cl-ctrl-testtype-value": list(),
651 "cl-ctrl-testtype-all-value": list(),
652 "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
653 "btn-ctrl-add-disabled": True,
655 elif trigger_id == "dd-ctrl-test":
657 framesize_opts = list()
658 testtype_opts = list()
659 phy = ctrl_panel.get("dd-ctrl-phy-value")
660 area = ctrl_panel.get("dd-ctrl-area-value")
661 if phy and area and dd_test:
663 {"label": v, "value": v}
664 for v in self.spec_tbs[phy][area]["core"]
667 {"label": v, "value": v}
668 for v in self.spec_tbs[phy][area]["frame-size"]
671 {"label": v, "value": v}
672 for v in self.spec_tbs[phy][area]["test-type"]
675 "dd-ctrl-test-value": dd_test,
676 "cl-ctrl-core-options": core_opts,
677 "cl-ctrl-core-value": list(),
678 "cl-ctrl-core-all-value": list(),
679 "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
680 "cl-ctrl-framesize-options": framesize_opts,
681 "cl-ctrl-framesize-value": list(),
682 "cl-ctrl-framesize-all-value": list(),
683 "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
684 "cl-ctrl-testtype-options": testtype_opts,
685 "cl-ctrl-testtype-value": list(),
686 "cl-ctrl-testtype-all-value": list(),
687 "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
688 "btn-ctrl-add-disabled": False,
690 elif trigger_id == "cl-ctrl-core":
691 val_sel, val_all = self._sync_checklists(
692 opt=ctrl_panel.get("cl-ctrl-core-options"),
698 "cl-ctrl-core-value": val_sel,
699 "cl-ctrl-core-all-value": val_all,
701 elif trigger_id == "cl-ctrl-core-all":
702 val_sel, val_all = self._sync_checklists(
703 opt = ctrl_panel.get("cl-ctrl-core-options"),
709 "cl-ctrl-core-value": val_sel,
710 "cl-ctrl-core-all-value": val_all,
712 elif trigger_id == "cl-ctrl-framesize":
713 val_sel, val_all = self._sync_checklists(
714 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
720 "cl-ctrl-framesize-value": val_sel,
721 "cl-ctrl-framesize-all-value": val_all,
723 elif trigger_id == "cl-ctrl-framesize-all":
724 val_sel, val_all = self._sync_checklists(
725 opt = ctrl_panel.get("cl-ctrl-framesize-options"),
727 all=cl_framesize_all,
731 "cl-ctrl-framesize-value": val_sel,
732 "cl-ctrl-framesize-all-value": val_all,
734 elif trigger_id == "cl-ctrl-testtype":
735 val_sel, val_all = self._sync_checklists(
736 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
742 "cl-ctrl-testtype-value": val_sel,
743 "cl-ctrl-testtype-all-value": val_all,
745 elif trigger_id == "cl-ctrl-testtype-all":
746 val_sel, val_all = self._sync_checklists(
747 opt = ctrl_panel.get("cl-ctrl-testtype-options"),
753 "cl-ctrl-testtype-value": val_sel,
754 "cl-ctrl-testtype-all-value": val_all,
756 elif trigger_id == "btn-ctrl-add":
758 phy = ctrl_panel.get("dd-ctrl-phy-value")
759 area = ctrl_panel.get("dd-ctrl-area-value")
760 test = ctrl_panel.get("dd-ctrl-test-value")
761 cores = ctrl_panel.get("cl-ctrl-core-value")
762 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
763 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
764 # Add selected test to the list of tests in store:
765 if phy and area and test and cores and framesizes and testtypes:
766 if store_sel is None:
769 for framesize in framesizes:
770 for ttype in testtypes:
772 f"{phy.replace('af_xdp', 'af-xdp')}-"
774 f"{framesize.lower()}-"
779 if tid not in [itm["id"] for itm in store_sel]:
785 "framesize": framesize.lower(),
786 "core": core.lower(),
787 "testtype": ttype.lower()
789 ctrl_panel.set(ctrl_panel.defaults)
791 "cl-selected-options": self._list_tests(store_sel)
793 elif trigger_id in ("btn-sel-display", "dpr-period"):
795 fig_tput, fig_lat = graph_trending(
796 self.data, store_sel, self.layout, d_start, d_end
798 fig_tput = fig_tput if fig_tput else self.NO_GRAPH
799 fig_lat = fig_lat if fig_lat else self.NO_GRAPH
800 elif trigger_id == "btn-sel-remove-all":
802 fig_tput = self.NO_GRAPH
803 fig_lat = self.NO_GRAPH
806 "cl-selected-options": list()
808 elif trigger_id == "btn-sel-remove":
811 new_store_sel = list()
812 for item in store_sel:
813 if item["id"] not in list_sel:
814 new_store_sel.append(item)
815 store_sel = new_store_sel
817 fig_tput, fig_lat = graph_trending(
818 self.data, store_sel, self.layout, d_start, d_end
820 fig_tput = fig_tput if fig_tput else self.NO_GRAPH
821 fig_lat = fig_lat if fig_lat else self.NO_GRAPH
823 "cl-selected-options": self._list_tests(store_sel)
826 fig_tput = self.NO_GRAPH
827 fig_lat = self.NO_GRAPH
830 "cl-selected-options": list()
833 ret_val = [ctrl_panel.panel, store_sel, fig_tput, fig_lat]
834 ret_val.extend(ctrl_panel.values())
838 Output("metadata-tput-lat", "children"),
839 Output("metadata-hdrh-graph", "children"),
840 Output("offcanvas-metadata", "is_open"),
841 Input("graph-tput", "clickData"),
842 Input("graph-latency", "clickData")
844 def _show_metadata_from_graphs(
845 tput_data: dict, lat_data: dict) -> tuple:
848 if not (tput_data or lat_data):
854 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
855 if trigger_id == "graph-tput":
857 txt = tput_data["points"][0]["text"].replace("<br>", "\n")
858 elif trigger_id == "graph-latency":
860 txt = lat_data["points"][0]["text"].replace("<br>", "\n")
861 hdrh_data = lat_data["points"][0].get("customdata", None)
864 id="hdrh-latency-graph",
865 figure=graph_hdrh_latency(hdrh_data, self.layout)
871 dbc.CardHeader(children=[
873 target_id="tput-lat-metadata",
875 style={"display": "inline-block"}
880 id="tput-lat-metadata",
887 return metadata, graph, True
890 Output("download-data", "data"),
891 State("selected-tests", "data"),
892 Input("btn-download-data", "n_clicks"),
893 prevent_initial_call=True
895 def _download_data(store_sel, n_clicks):
903 for itm in store_sel:
904 sel_data = select_trending_data(self.data, itm)
907 df = pd.concat([df, sel_data], ignore_index=True)
909 return dcc.send_data_frame(df.to_csv, "trending_data.csv")