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.
15 """Plotly Dash HTML layout override.
20 import dash_bootstrap_components as dbc
22 from flask import Flask
25 from dash import callback_context, no_update, ALL
26 from dash import Input, Output, State
27 from dash.exceptions import PreventUpdate
28 from yaml import load, FullLoader, YAMLError
29 from ast import literal_eval
31 from ..utils.constants import Constants as C
32 from ..utils.control_panel import ControlPanel
33 from ..utils.trigger import Trigger
34 from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
35 generate_options, get_list_group_items
36 from ..utils.url_processing import url_decode
37 from ..data.data import Data
38 from .graphs import graph_trending, graph_hdrh_latency, select_trending_data
41 # Control panel partameters and their default values.
47 "dd-area-opt": list(),
50 "dd-test-opt": list(),
53 "cl-core-opt": list(),
54 "cl-core-val": list(),
55 "cl-core-all-val": list(),
56 "cl-core-all-opt": C.CL_ALL_DISABLED,
57 "cl-frmsize-opt": list(),
58 "cl-frmsize-val": list(),
59 "cl-frmsize-all-val": list(),
60 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
61 "cl-tsttype-opt": list(),
62 "cl-tsttype-val": list(),
63 "cl-tsttype-all-val": list(),
64 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
66 "cl-normalize-val": list()
71 """The layout of the dash app and the callbacks.
74 def __init__(self, app: Flask, html_layout_file: str,
75 graph_layout_file: str, data_spec_file: str, tooltip_file: str,
76 time_period: str=None) -> None:
78 - save the input parameters,
79 - read and pre-process the data,
80 - prepare data for the control panel,
81 - read HTML layout file,
82 - read tooltips from the tooltip file.
84 :param app: Flask application running the dash application.
85 :param html_layout_file: Path and name of the file specifying the HTML
86 layout of the dash application.
87 :param graph_layout_file: Path and name of the file with layout of
89 :param data_spec_file: Path and name of the file specifying the data to
90 be read from parquets for this application.
91 :param tooltip_file: Path and name of the yaml file specifying the
93 :param time_period: It defines the time period for data read from the
94 parquets in days from now back to the past.
96 :type html_layout_file: str
97 :type graph_layout_file: str
98 :type data_spec_file: str
99 :type tooltip_file: str
100 :type time_period: int
105 self._html_layout_file = html_layout_file
106 self._graph_layout_file = graph_layout_file
107 self._data_spec_file = data_spec_file
108 self._tooltip_file = tooltip_file
109 self._time_period = time_period
113 data_spec_file=self._data_spec_file,
115 ).read_trending_mrr(days=self._time_period)
118 data_spec_file=self._data_spec_file,
120 ).read_trending_ndrpdr(days=self._time_period)
122 self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
124 # Get structure of tests:
126 for _, row in self._data[["job", "test_id"]].drop_duplicates().\
128 lst_job = row["job"].split("-")
131 tbed = "-".join(lst_job[-2:])
132 lst_test = row["test_id"].split(".")
136 area = "-".join(lst_test[3:-2])
137 suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\
140 nic = suite.split("-")[0]
141 for drv in C.DRIVERS:
147 test = test.replace(f"{drv}-", "")
151 infra = "-".join((tbed, nic, driver))
152 lst_test = test.split("-")
153 framesize = lst_test[0]
154 core = lst_test[1] if lst_test[1] else "8C"
155 test = "-".join(lst_test[2: -1])
157 if tbs.get(dut, None) is None:
159 if tbs[dut].get(infra, None) is None:
160 tbs[dut][infra] = dict()
161 if tbs[dut][infra].get(area, None) is None:
162 tbs[dut][infra][area] = dict()
163 if tbs[dut][infra][area].get(test, None) is None:
164 tbs[dut][infra][area][test] = dict()
165 tbs[dut][infra][area][test]["core"] = list()
166 tbs[dut][infra][area][test]["frame-size"] = list()
167 tbs[dut][infra][area][test]["test-type"] = list()
168 if core.upper() not in tbs[dut][infra][area][test]["core"]:
169 tbs[dut][infra][area][test]["core"].append(core.upper())
170 if framesize.upper() not in \
171 tbs[dut][infra][area][test]["frame-size"]:
172 tbs[dut][infra][area][test]["frame-size"].append(
176 if "MRR" not in tbs[dut][infra][area][test]["test-type"]:
177 tbs[dut][infra][area][test]["test-type"].append("MRR")
178 elif ttype == "ndrpdr":
179 if "NDR" not in tbs[dut][infra][area][test]["test-type"]:
180 tbs[dut][infra][area][test]["test-type"].extend(
186 self._html_layout = str()
187 self._graph_layout = None
188 self._tooltips = dict()
191 with open(self._html_layout_file, "r") as file_read:
192 self._html_layout = file_read.read()
193 except IOError as err:
195 f"Not possible to open the file {self._html_layout_file}\n{err}"
199 with open(self._graph_layout_file, "r") as file_read:
200 self._graph_layout = load(file_read, Loader=FullLoader)
201 except IOError as err:
203 f"Not possible to open the file {self._graph_layout_file}\n"
206 except YAMLError as err:
208 f"An error occurred while parsing the specification file "
209 f"{self._graph_layout_file}\n{err}"
213 with open(self._tooltip_file, "r") as file_read:
214 self._tooltips = load(file_read, Loader=FullLoader)
215 except IOError as err:
217 f"Not possible to open the file {self._tooltip_file}\n{err}"
219 except YAMLError as err:
221 f"An error occurred while parsing the specification file "
222 f"{self._tooltip_file}\n{err}"
226 if self._app is not None and hasattr(self, "callbacks"):
227 self.callbacks(self._app)
230 def html_layout(self):
231 return self._html_layout
233 def add_content(self):
234 """Top level method which generated the web page.
237 - Store for user input data,
239 - Main area with control panel and ploting area.
241 If no HTML layout is provided, an error message is displayed instead.
243 :returns: The HTML div with the whole page.
247 if self.html_layout and self._spec_tbs:
262 id="offcanvas-metadata",
263 title="Throughput And Latency",
267 dbc.Row(id="metadata-tput-lat"),
268 dbc.Row(id="metadata-hdrh-graph")
276 dcc.Store(id="store-selected-tests"),
277 dcc.Store(id="store-control-panel"),
278 dcc.Location(id="url", refresh=False),
279 self._add_ctrl_col(),
280 self._add_plotting_col()
298 def _add_navbar(self):
299 """Add nav element with navigation panel. It is placed on the top.
301 :returns: Navigation bar.
302 :rtype: dbc.NavbarSimple
304 return dbc.NavbarSimple(
305 id="navbarsimple-main",
318 brand_external_link=True,
323 def _add_ctrl_col(self) -> dbc.Col:
324 """Add column with controls. It is placed on the left side.
326 :returns: Column with the control panel.
331 children=self._add_ctrl_panel(),
332 className="sticky-top"
336 def _add_plotting_col(self) -> dbc.Col:
337 """Add column with plots and tables. It is placed on the right side.
339 :returns: Column with tables.
343 id="col-plotting-area",
349 class_name="g-0 p-0",
360 def _add_ctrl_panel(self) -> list:
361 """Add control panel.
363 :returns: Control panel.
368 class_name="g-0 p-1",
373 children=show_tooltip(
380 id={"type": "ctrl-dd", "index": "dut"},
381 placeholder="Select a Device under Test...",
384 {"label": k, "value": k} \
385 for k in self._spec_tbs.keys()
387 key=lambda d: d["label"]
396 class_name="g-0 p-1",
401 children=show_tooltip(
408 id={"type": "ctrl-dd", "index": "phy"},
410 "Select a Physical Test Bed Topology..."
418 class_name="g-0 p-1",
423 children=show_tooltip(
430 id={"type": "ctrl-dd", "index": "area"},
431 placeholder="Select an Area..."
439 class_name="g-0 p-1",
444 children=show_tooltip(
451 id={"type": "ctrl-dd", "index": "test"},
452 placeholder="Select a Test..."
460 class_name="g-0 p-1",
463 children=show_tooltip(
472 id={"type": "ctrl-cl", "index": "frmsize-all"},
473 options=C.CL_ALL_DISABLED,
476 input_class_name="border-info bg-info"
484 id={"type": "ctrl-cl", "index": "frmsize"},
487 input_class_name="border-info bg-info"
494 class_name="g-0 p-1",
497 children=show_tooltip(
506 id={"type": "ctrl-cl", "index": "core-all"},
507 options=C.CL_ALL_DISABLED,
510 input_class_name="border-info bg-info"
518 id={"type": "ctrl-cl", "index": "core"},
521 input_class_name="border-info bg-info"
528 class_name="g-0 p-1",
531 children=show_tooltip(
540 id={"type": "ctrl-cl", "index": "tsttype-all"},
541 options=C.CL_ALL_DISABLED,
544 input_class_name="border-info bg-info"
552 id={"type": "ctrl-cl", "index": "tsttype"},
555 input_class_name="border-info bg-info"
562 class_name="g-0 p-1",
565 children=show_tooltip(
577 "value": "normalize",
579 "Normalize results to CPU "
587 input_class_name="border-info bg-info"
594 class_name="g-0 p-1",
597 id={"type": "ctrl-btn", "index": "add-test"},
598 children="Add Selected",
604 id="row-card-sel-tests",
605 class_name="g-0 p-1",
606 style=C.STYLE_DISABLED,
609 class_name="overflow-auto p-0",
612 style={"max-height": "14em"},
618 id="row-btns-sel-tests",
619 class_name="g-0 p-1",
620 style=C.STYLE_DISABLED,
625 id={"type": "ctrl-btn", "index": "rm-test"},
626 children="Remove Selected",
632 id={"type": "ctrl-btn", "index": "rm-test-all"},
633 children="Remove All",
644 def _get_plotting_area(
650 """Generate the plotting area with all its content.
655 figs = graph_trending(self._data, tests, self._graph_layout, normalize)
663 id={"type": "graph", "index": "tput"},
675 id={"type": "graph", "index": "lat"},
688 active_tab="tab-tput",
701 "text-transform": "none",
702 "padding": "0rem 1rem"
707 dbc.ModalHeader(dbc.ModalTitle("URL")),
716 id="plot-btn-download",
717 children="Download Data",
721 "text-transform": "none",
722 "padding": "0rem 1rem"
725 dcc.Download(id="download-trending-data")
728 "d-grid gap-0 d-md-flex justify-content-md-end"
747 class_name="g-0 p-1",
748 start_collapsed=False,
750 active_item=[f"item-{i}" for i in range(len(acc_items))]
752 class_name="g-0 p-0",
758 # id="btn-add-telemetry",
759 # children="Add Panel with Telemetry",
763 # "text-transform": "none",
764 # "padding": "0rem 1rem"
769 # "d-grid gap-0 d-md-flex justify-content-md-end"
771 # class_name="g-0 p-0"
776 def callbacks(self, app):
777 """Callbacks for the whole application.
779 :param app: The application.
785 Output("store-control-panel", "data"),
786 Output("store-selected-tests", "data"),
787 Output("plotting-area", "children"),
788 Output("row-card-sel-tests", "style"),
789 Output("row-btns-sel-tests", "style"),
790 Output("lg-selected", "children"),
792 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
793 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
794 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
795 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
796 Output({"type": "ctrl-dd", "index": "area"}, "options"),
797 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
798 Output({"type": "ctrl-dd", "index": "area"}, "value"),
799 Output({"type": "ctrl-dd", "index": "test"}, "options"),
800 Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
801 Output({"type": "ctrl-dd", "index": "test"}, "value"),
802 Output({"type": "ctrl-cl", "index": "core"}, "options"),
803 Output({"type": "ctrl-cl", "index": "core"}, "value"),
804 Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
805 Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
806 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
807 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
808 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
809 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
810 Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
811 Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
812 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
813 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
814 Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
815 Output("normalize", "value")
818 State("store-control-panel", "data"),
819 State("store-selected-tests", "data"),
820 State({"type": "sel-cl", "index": ALL}, "value")
823 Input("url", "href"),
824 Input("normalize", "value"),
826 Input({"type": "ctrl-dd", "index": ALL}, "value"),
827 Input({"type": "ctrl-cl", "index": ALL}, "value"),
828 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
831 def _update_application(
839 """Update the application when the event is detected.
842 ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
846 parsed_url = url_decode(href)
848 url_params = parsed_url["params"]
852 plotting_area = no_update
853 row_card_sel_tests = no_update
854 row_btns_sel_tests = no_update
855 lg_selected = no_update
857 trigger = Trigger(callback_context.triggered)
859 if trigger.type == "url" and url_params:
861 store_sel = literal_eval(url_params["store_sel"][0])
862 normalize = literal_eval(url_params["norm"][0])
863 except (KeyError, IndexError):
866 last_test = store_sel[-1]
867 test = self._spec_tbs[last_test["dut"]][last_test["phy"]]\
868 [last_test["area"]][last_test["test"]]
870 "dd-dut-val": last_test["dut"],
871 "dd-phy-val": last_test["phy"],
872 "dd-phy-opt": generate_options(
873 self._spec_tbs[last_test["dut"]].keys()
876 "dd-area-val": last_test["area"],
878 {"label": label(v), "value": v} for v in sorted(
879 self._spec_tbs[last_test["dut"]]\
880 [last_test["phy"]].keys()
883 "dd-area-dis": False,
884 "dd-test-val": last_test["test"],
885 "dd-test-opt": generate_options(
886 self._spec_tbs[last_test["dut"]][last_test["phy"]]\
887 [last_test["area"]].keys()
889 "dd-test-dis": False,
890 "cl-core-opt": generate_options(test["core"]),
891 "cl-core-val": [last_test["core"].upper(), ],
892 "cl-core-all-val": list(),
893 "cl-core-all-opt": C.CL_ALL_ENABLED,
894 "cl-frmsize-opt": generate_options(test["frame-size"]),
895 "cl-frmsize-val": [last_test["framesize"].upper(), ],
896 "cl-frmsize-all-val": list(),
897 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
898 "cl-tsttype-opt": generate_options(test["test-type"]),
899 "cl-tsttype-val": [last_test["testtype"].upper(), ],
900 "cl-tsttype-all-val": list(),
901 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
902 "cl-normalize-val": normalize,
906 elif trigger.type == "normalize":
907 ctrl_panel.set({"cl-normalize-val": normalize})
909 elif trigger.type == "ctrl-dd":
910 if trigger.idx == "dut":
912 options = generate_options(
913 self._spec_tbs[trigger.value].keys()
920 "dd-dut-val": trigger.value,
922 "dd-phy-opt": options,
923 "dd-phy-dis": disabled,
924 "dd-area-val": str(),
925 "dd-area-opt": list(),
927 "dd-test-val": str(),
928 "dd-test-opt": list(),
930 "cl-core-opt": list(),
931 "cl-core-val": list(),
932 "cl-core-all-val": list(),
933 "cl-core-all-opt": C.CL_ALL_DISABLED,
934 "cl-frmsize-opt": list(),
935 "cl-frmsize-val": list(),
936 "cl-frmsize-all-val": list(),
937 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
938 "cl-tsttype-opt": list(),
939 "cl-tsttype-val": list(),
940 "cl-tsttype-all-val": list(),
941 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
944 elif trigger.idx == "phy":
946 dut = ctrl_panel.get("dd-dut-val")
947 phy = self._spec_tbs[dut][trigger.value]
948 options = [{"label": label(v), "value": v} \
949 for v in sorted(phy.keys())]
955 "dd-phy-val": trigger.value,
956 "dd-area-val": str(),
957 "dd-area-opt": options,
958 "dd-area-dis": disabled,
959 "dd-test-val": str(),
960 "dd-test-opt": list(),
962 "cl-core-opt": list(),
963 "cl-core-val": list(),
964 "cl-core-all-val": list(),
965 "cl-core-all-opt": C.CL_ALL_DISABLED,
966 "cl-frmsize-opt": list(),
967 "cl-frmsize-val": list(),
968 "cl-frmsize-all-val": list(),
969 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
970 "cl-tsttype-opt": list(),
971 "cl-tsttype-val": list(),
972 "cl-tsttype-all-val": list(),
973 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
976 elif trigger.idx == "area":
978 dut = ctrl_panel.get("dd-dut-val")
979 phy = ctrl_panel.get("dd-phy-val")
980 area = self._spec_tbs[dut][phy][trigger.value]
981 options = generate_options(area.keys())
987 "dd-area-val": trigger.value,
988 "dd-test-val": str(),
989 "dd-test-opt": options,
990 "dd-test-dis": disabled,
991 "cl-core-opt": list(),
992 "cl-core-val": list(),
993 "cl-core-all-val": list(),
994 "cl-core-all-opt": C.CL_ALL_DISABLED,
995 "cl-frmsize-opt": list(),
996 "cl-frmsize-val": list(),
997 "cl-frmsize-all-val": list(),
998 "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
999 "cl-tsttype-opt": list(),
1000 "cl-tsttype-val": list(),
1001 "cl-tsttype-all-val": list(),
1002 "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1005 elif trigger.idx == "test":
1006 dut = ctrl_panel.get("dd-dut-val")
1007 phy = ctrl_panel.get("dd-phy-val")
1008 area = ctrl_panel.get("dd-area-val")
1009 if all((dut, phy, area, trigger.value, )):
1010 test = self._spec_tbs[dut][phy][area][trigger.value]
1012 "dd-test-val": trigger.value,
1013 "cl-core-opt": generate_options(test["core"]),
1014 "cl-core-val": list(),
1015 "cl-core-all-val": list(),
1016 "cl-core-all-opt": C.CL_ALL_ENABLED,
1018 generate_options(test["frame-size"]),
1019 "cl-frmsize-val": list(),
1020 "cl-frmsize-all-val": list(),
1021 "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1023 generate_options(test["test-type"]),
1024 "cl-tsttype-val": list(),
1025 "cl-tsttype-all-val": list(),
1026 "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1029 elif trigger.type == "ctrl-cl":
1030 param = trigger.idx.split("-")[0]
1031 if "-all" in trigger.idx:
1032 c_sel, c_all, c_id = list(), trigger.value, "all"
1034 c_sel, c_all, c_id = trigger.value, list(), str()
1035 val_sel, val_all = sync_checklists(
1036 options=ctrl_panel.get(f"cl-{param}-opt"),
1042 f"cl-{param}-val": val_sel,
1043 f"cl-{param}-all-val": val_all,
1045 if all((ctrl_panel.get("cl-core-val"),
1046 ctrl_panel.get("cl-frmsize-val"),
1047 ctrl_panel.get("cl-tsttype-val"), )):
1048 ctrl_panel.set({"btn-add-dis": False})
1050 ctrl_panel.set({"btn-add-dis": True})
1051 elif trigger.type == "ctrl-btn":
1053 if trigger.idx == "add-test":
1054 dut = ctrl_panel.get("dd-dut-val")
1055 phy = ctrl_panel.get("dd-phy-val")
1056 area = ctrl_panel.get("dd-area-val")
1057 test = ctrl_panel.get("dd-test-val")
1058 # Add selected test(s) to the list of tests in store:
1059 if store_sel is None:
1061 for core in ctrl_panel.get("cl-core-val"):
1062 for framesize in ctrl_panel.get("cl-frmsize-val"):
1063 for ttype in ctrl_panel.get("cl-tsttype-val"):
1068 phy.replace('af_xdp', 'af-xdp'),
1075 if tid not in [i["id"] for i in store_sel]:
1082 "framesize": framesize.lower(),
1083 "core": core.lower(),
1084 "testtype": ttype.lower()
1086 store_sel = sorted(store_sel, key=lambda d: d["id"])
1087 if C.CLEAR_ALL_INPUTS:
1088 ctrl_panel.set(ctrl_panel.defaults)
1089 elif trigger.idx == "rm-test" and lst_sel:
1090 new_store_sel = list()
1091 for idx, item in enumerate(store_sel):
1092 if not lst_sel[idx]:
1093 new_store_sel.append(item)
1094 store_sel = new_store_sel
1095 elif trigger.idx == "rm-test-all":
1100 lg_selected = get_list_group_items(store_sel)
1101 plotting_area = self._get_plotting_area(
1106 {"store_sel": store_sel, "norm": normalize}
1109 row_card_sel_tests = C.STYLE_ENABLED
1110 row_btns_sel_tests = C.STYLE_ENABLED
1112 plotting_area = C.PLACEHOLDER
1113 row_card_sel_tests = C.STYLE_DISABLED
1114 row_btns_sel_tests = C.STYLE_DISABLED
1125 ret_val.extend(ctrl_panel.values)
1129 Output("plot-mod-url", "is_open"),
1130 [Input("plot-btn-url", "n_clicks")],
1131 [State("plot-mod-url", "is_open")],
1133 def toggle_plot_mod_url(n, is_open):
1134 """Toggle the modal window with url.
1141 Output("metadata-tput-lat", "children"),
1142 Output("metadata-hdrh-graph", "children"),
1143 Output("offcanvas-metadata", "is_open"),
1144 Input({"type": "graph", "index": ALL}, "clickData"),
1145 prevent_initial_call=True
1147 def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1148 """Generates the data for the offcanvas displayed when a particular
1149 point in a graph is clicked on.
1151 :param graph_data: The data from the clicked point in the graph.
1152 :type graph_data: dict
1153 :returns: The data to be displayed on the offcanvas and the
1154 information to show the offcanvas.
1155 :rtype: tuple(list, list, bool)
1158 trigger = Trigger(callback_context.triggered)
1161 idx = 0 if trigger.idx == "tput" else 1
1162 graph_data = graph_data[idx]["points"][0]
1163 except (IndexError, KeyError, ValueError, TypeError):
1166 metadata = no_update
1171 [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1172 ) for x in graph_data.get("text", "").split("<br>")
1174 if trigger.idx == "tput":
1175 title = "Throughput"
1176 elif trigger.idx == "lat":
1178 hdrh_data = graph_data.get("customdata", None)
1181 class_name="gy-2 p-0",
1183 dbc.CardHeader(hdrh_data.pop("name")),
1184 dbc.CardBody(children=[
1186 id="hdrh-latency-graph",
1187 figure=graph_hdrh_latency(
1188 hdrh_data, self._graph_layout
1199 class_name="gy-2 p-0",
1201 dbc.CardHeader(children=[
1203 target_id="tput-lat-metadata",
1205 style={"display": "inline-block"}
1210 id="tput-lat-metadata",
1212 children=[dbc.ListGroup(children, flush=True), ]
1218 return metadata, graph, True
1221 Output("download-trending-data", "data"),
1222 State("store-selected-tests", "data"),
1223 Input("plot-btn-download", "n_clicks"),
1224 prevent_initial_call=True
1226 def _download_trending_data(store_sel, _):
1227 """Download the data
1229 :param store_sel: List of tests selected by user stored in the
1231 :type store_sel: list
1232 :returns: dict of data frame content (base64 encoded) and meta data
1233 used by the Download component.
1241 for itm in store_sel:
1242 sel_data = select_trending_data(self._data, itm)
1243 if sel_data is None:
1245 df = pd.concat([df, sel_data], ignore_index=True)
1247 return dcc.send_data_frame(df.to_csv, C.TREND_DOWNLOAD_FILE_NAME)