feat(uti): re-work the callbacks
[csit.git] / resources / tools / dash / app / pal / trending / layout.py
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:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
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.
13
14 """Plotly Dash HTML layout override.
15 """
16
17 import pandas as pd
18 import dash_bootstrap_components as dbc
19
20 from dash import dcc
21 from dash import html
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
28
29 from ..data.data import Data
30 from .graphs import graph_trending, graph_hdrh_latency, \
31     select_trending_data
32
33
34 class Layout:
35     """
36     """
37
38     NO_GRAPH = {"data": [], "layout": {}, "frames": []}
39
40     CL_ALL_DISABLED = [{
41         "label": "All",
42         "value": "all",
43         "disabled": True
44     }]
45
46     CL_ALL_ENABLED = [{
47         "label": "All",
48         "value": "all",
49         "disabled": False
50     }]
51
52     def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
53         data_spec_file):
54         """
55         """
56
57         # Inputs
58         self._app = app
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
63
64         # Read the data:
65         data_mrr = Data(
66             data_spec_file=self._data_spec_file,
67             debug=True
68         ).read_trending_mrr(days=5)
69
70         data_ndrpdr = Data(
71             data_spec_file=self._data_spec_file,
72             debug=True
73         ).read_trending_ndrpdr(days=14)
74
75         self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
76
77         # Read from files:
78         self._html_layout = ""
79         self._spec_tbs = None
80         self._graph_layout = None
81
82         try:
83             with open(self._html_layout_file, "r") as file_read:
84                 self._html_layout = file_read.read()
85         except IOError as err:
86             raise RuntimeError(
87                 f"Not possible to open the file {self._html_layout_file}\n{err}"
88             )
89
90         try:
91             with open(self._spec_file, "r") as file_read:
92                 self._spec_tbs = load(file_read, Loader=FullLoader)
93         except IOError as err:
94             raise RuntimeError(
95                 f"Not possible to open the file {self._spec_file,}\n{err}"
96             )
97         except YAMLError as err:
98             raise RuntimeError(
99                 f"An error occurred while parsing the specification file "
100                 f"{self._spec_file,}\n"
101                 f"{err}"
102             )
103
104         try:
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:
108             raise RuntimeError(
109                 f"Not possible to open the file {self._graph_layout_file}\n"
110                 f"{err}"
111             )
112         except YAMLError as err:
113             raise RuntimeError(
114                 f"An error occurred while parsing the specification file "
115                 f"{self._graph_layout_file}\n"
116                 f"{err}"
117             )
118
119         # Callbacks:
120         if self._app is not None and hasattr(self, 'callbacks'):
121             self.callbacks(self._app)
122
123     @property
124     def html_layout(self):
125         return self._html_layout
126
127     @property
128     def spec_tbs(self):
129         return self._spec_tbs
130
131     @property
132     def data(self):
133         return self._data
134
135     @property
136     def layout(self):
137         return self._graph_layout
138
139     def add_content(self):
140         """
141         """
142         if self.html_layout and self.spec_tbs:
143             return html.Div(
144                 id="div-main",
145                 children=[
146                     dbc.Row(
147                         id="row-navbar",
148                         class_name="g-0",
149                         children=[
150                             self._add_navbar(),
151                         ]
152                     ),
153                     dcc.Loading(
154                         dbc.Offcanvas(
155                             id="offcanvas-metadata",
156                             title="Throughput And Latency",
157                             placement="end",
158                             is_open=False,
159                             children=[
160                                 dbc.Row(id="metadata-tput-lat"),
161                                 dbc.Row(id="metadata-hdrh-graph"),
162                             ]
163                         )
164                     ),
165                     dbc.Row(
166                         id="row-main",
167                         class_name="g-0 p-2",
168                         children=[
169                             dcc.Store(
170                                 id="selected-tests"
171                             ),
172                             dcc.Store(
173                                 id="control-panel"
174                             ),
175                             self._add_ctrl_col(),
176                             self._add_plotting_col(),
177                         ]
178                     )
179                 ]
180             )
181         else:
182             return html.Div(
183                 id="div-main-error",
184                 children=[
185                     dbc.Alert(
186                         [
187                             "An Error Occured",
188                         ],
189                         color="danger",
190                     ),
191                 ]
192             )
193
194     def _add_navbar(self):
195         """Add nav element with navigation panel. It is placed on the top.
196         """
197         return dbc.NavbarSimple(
198             id="navbarsimple-main",
199             children=[
200                 dbc.NavItem(
201                     dbc.NavLink(
202                         "Continuous Performance Trending",
203                         disabled=True,
204                         external_link=True,
205                         href="#"
206                     )
207                 )
208             ],
209             brand="Dashboard",
210             brand_href="/",
211             brand_external_link=True,
212             class_name="p-2",
213             fluid=True,
214         )
215
216     def _add_ctrl_col(self) -> dbc.Col:
217         """Add column with controls. It is placed on the left side.
218         """
219         return dbc.Col(
220             id="col-controls",
221             children=[
222                 self._add_ctrl_panel(),
223                 self._add_ctrl_shown()
224             ],
225         )
226
227     def _add_plotting_col(self) -> dbc.Col:
228         """Add column with plots and tables. It is placed on the right side.
229         """
230         return dbc.Col(
231             id="col-plotting-area",
232             children=[
233                 dbc.Row(  # Throughput
234                     id="row-graph-tput",
235                     class_name="g-0 p-2",
236                     children=[
237                         dcc.Loading(
238                             dcc.Graph(id="graph-tput")
239                         )
240                     ]
241                 ),
242                 dbc.Row(  # Latency
243                     id="row-graph-lat",
244                     class_name="g-0 p-2",
245                     children=[
246                         dcc.Loading(
247                             dcc.Graph(id="graph-latency")
248                         )
249                     ]
250                 ),
251                 dbc.Row(  # Download
252                     id="div-download",
253                     class_name="g-0",
254                     children=[
255                         dcc.Loading(children=[
256                             dbc.Button(
257                                 id="btn-download-data",
258                                 children=["Download Data"]
259                             ),
260                             dcc.Download(id="download-data")
261                         ])
262                     ]
263                 )
264             ],
265             width=9,
266         )
267
268     def _add_ctrl_panel(self) -> dbc.Row:
269         """
270         """
271         return dbc.Row(
272             id="row-ctrl-panel",
273             class_name="g-0",
274             children=[
275                 dbc.Label("Physical Test Bed Topology, NIC and Driver"),
276                 dbc.Select(
277                     id="dd-ctrl-phy",
278                     className="p-2",
279                     placeholder="Select a Physical Test Bed Topology...",
280                     options=[
281                         {"label": k, "value": k} for k in self.spec_tbs.keys()
282                     ],
283                     size="sm",
284                 ),
285                 dbc.Label("Area"),
286                 dbc.Select(
287                     id="dd-ctrl-area",
288                     className="p-2",
289                     placeholder="Select an Area...",
290                     disabled=True,
291                     size="sm",
292                 ),
293                 dbc.Label("Test"),
294                 dbc.Select(
295                     id="dd-ctrl-test",
296                     className="p-2",
297                     placeholder="Select a Test...",
298                     disabled=True,
299                     size="sm",
300                 ),
301                 dbc.Row(
302                     id="row-ctrl-core",
303                     class_name="g-0",
304                     children=[
305                         dbc.Label("Number of Cores"),
306                         dbc.Col([
307                             dbc.Checklist(
308                                 id="cl-ctrl-core-all",
309                                 options=self.CL_ALL_DISABLED,
310                                 inline=True,
311                                 switch=False
312                             ),
313                         ], width=3),
314                         dbc.Col([
315                             dbc.Checklist(
316                                 id="cl-ctrl-core",
317                                 inline=True,
318                                 switch=False
319                             )
320                         ])
321                     ]
322                 ),
323                 dbc.Row(
324                     id="row-ctrl-framesize",
325                     class_name="g-0",
326                     children=[
327                         dbc.Label("Frame Size"),
328                         dbc.Col([
329                             dbc.Checklist(
330                                 id="cl-ctrl-framesize-all",
331                                 options=self.CL_ALL_DISABLED,
332                                 inline=True,
333                                 switch=False
334                             ),
335                         ], width=3),
336                         dbc.Col([
337                             dbc.Checklist(
338                                 id="cl-ctrl-framesize",
339                                 inline=True,
340                                 switch=False
341                             )
342                         ])
343                     ]
344                 ),
345                 dbc.Row(
346                     id="row-ctrl-testtype",
347                     class_name="g-0",
348                     children=[
349                         dbc.Label("Test Type"),
350                         dbc.Col([
351                             dbc.Checklist(
352                                 id="cl-ctrl-testtype-all",
353                                 options=self.CL_ALL_DISABLED,
354                                 inline=True,
355                                 switch=False
356                             ),
357                         ], width=3),
358                         dbc.Col([
359                             dbc.Checklist(
360                                 id="cl-ctrl-testtype",
361                                 inline=True,
362                                 switch=False
363                             )
364                         ])
365                     ]
366                 ),
367                 dbc.Row(
368                     class_name="g-0",
369                     children=[
370                         dbc.Button(
371                             id="btn-ctrl-add",
372                             children="Add",
373                             disabled=True
374                         )
375                     ]
376                 ),
377                 dbc.Row(
378                     class_name="g-0",
379                     children=[
380                         dcc.DatePickerRange(
381                             id="dpr-period",
382                             min_date_allowed=\
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"
389                         )
390                     ]
391                 )
392             ]
393         )
394
395     def _add_ctrl_shown(self) -> dbc.Row:
396         """
397         """
398         return dbc.Row(
399             id="div-ctrl-shown",
400             class_name="g-0",
401             children=[
402                 dbc.Row(
403                     class_name="g-0",
404                     children=[
405                         dbc.Label("Selected tests"),
406                         dbc.Checklist(
407                             id="cl-selected",
408                             options=[],
409                             inline=False
410                         )
411                     ]
412                 ),
413                 dbc.Row(
414                     class_name="g-0",
415                     children=[
416                         dbc.ButtonGroup(
417                             [
418                                 dbc.Button(
419                                     id="btn-sel-remove-all",
420                                     children="Remove All",
421                                     color="secondary",
422                                     disabled=False
423                                 ),
424                                 dbc.Button(
425                                     id="btn-sel-remove",
426                                     children="Remove Selected",
427                                     color="secondary",
428                                     disabled=False
429                                 ),
430                                 dbc.Button(
431                                     id="btn-sel-display",
432                                     children="Display",
433                                     color="secondary",
434                                     disabled=False
435                                 )
436                             ],
437                             size="md",
438                             class_name="me-1",
439                         ),
440                     ]
441                 )
442             ]
443         )
444
445     class ControlPanel:
446         def __init__(self, panel: dict) -> None:
447
448             CL_ALL_DISABLED = [{
449                 "label": "All",
450                 "value": "all",
451                 "disabled": True
452             }]
453
454             # Defines also the order of keys
455             self._defaults = {
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(),
477             }
478
479             self._panel = deepcopy(self._defaults)
480             if panel:
481                 for key in self._defaults:
482                     self._panel[key] = panel[key]
483
484         @property
485         def defaults(self) -> dict:
486             return self._defaults
487
488         @property
489         def panel(self) -> dict:
490             return self._panel
491
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
496                 else:
497                     raise KeyError(f"The key {key} is not defined.")
498
499         def get(self, key: str) -> any:
500             return self._panel[key]
501
502         def values(self) -> tuple:
503             return tuple(self._panel.values())
504
505     @staticmethod
506     def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
507         """
508         """
509         options = {v["value"] for v in opt}
510         if id =="all":
511             sel = list(options) if all else list()
512         else:
513             all = ["all", ] if set(sel) == options else list()
514         return sel, all
515
516     @staticmethod
517     def _list_tests(selection: dict) -> list:
518         """Display selected tests with checkboxes
519         """
520         if selection:
521             return [
522                 {"label": v["id"], "value": v["id"]} for v in selection
523             ]
524         else:
525             return list()
526
527     def callbacks(self, app):
528
529         @app.callback(
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"),
573         )
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:
580             """
581             """
582
583             d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
584                 int(d_start[8:10]))
585             d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
586
587             fig_tput = no_update
588             fig_lat = no_update
589
590             ctrl_panel = self.ControlPanel(cp_data)
591
592             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
593
594             if trigger_id == "dd-ctrl-phy":
595                 try:
596                     options = [
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()]
599                     ]
600                     disabled = False
601                 except KeyError:
602                     options = list()
603                     disabled = no_update
604                 ctrl_panel.set({
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,
624                 })
625             elif trigger_id == "dd-ctrl-area":
626                 try:
627                     phy = ctrl_panel.get("dd-ctrl-phy-value")
628                     options = [
629                         {"label": v, "value": v}
630                             for v in self.spec_tbs[phy][dd_area]["test"]
631                     ]
632                     disabled = False
633                 except KeyError:
634                     options = list()
635                     disabled = True
636                 ctrl_panel.set({
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,
654                 })
655             elif trigger_id == "dd-ctrl-test":
656                 core_opts = list()
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:
662                     core_opts = [
663                         {"label": v, "value": v}
664                             for v in self.spec_tbs[phy][area]["core"]
665                     ]
666                     framesize_opts = [
667                         {"label": v, "value": v}
668                             for v in self.spec_tbs[phy][area]["frame-size"]
669                     ]
670                     testtype_opts = [
671                         {"label": v, "value": v}
672                             for v in self.spec_tbs[phy][area]["test-type"]
673                     ]
674                     ctrl_panel.set({
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,
689                     })
690             elif trigger_id == "cl-ctrl-core":
691                 val_sel, val_all = self._sync_checklists(
692                     opt=ctrl_panel.get("cl-ctrl-core-options"),
693                     sel=cl_core,
694                     all=list(),
695                     id=""
696                 )
697                 ctrl_panel.set({
698                     "cl-ctrl-core-value": val_sel,
699                     "cl-ctrl-core-all-value": val_all,
700                 })
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"),
704                     sel=list(),
705                     all=cl_core_all,
706                     id="all"
707                 )
708                 ctrl_panel.set({
709                     "cl-ctrl-core-value": val_sel,
710                     "cl-ctrl-core-all-value": val_all,
711                 })
712             elif trigger_id == "cl-ctrl-framesize":
713                 val_sel, val_all = self._sync_checklists(
714                     opt = ctrl_panel.get("cl-ctrl-framesize-options"),
715                     sel=cl_framesize,
716                     all=list(),
717                     id=""
718                 )
719                 ctrl_panel.set({
720                     "cl-ctrl-framesize-value": val_sel,
721                     "cl-ctrl-framesize-all-value": val_all,
722                 })
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"),
726                     sel=list(),
727                     all=cl_framesize_all,
728                     id="all"
729                 )
730                 ctrl_panel.set({
731                     "cl-ctrl-framesize-value": val_sel,
732                     "cl-ctrl-framesize-all-value": val_all,
733                 })
734             elif trigger_id == "cl-ctrl-testtype":
735                 val_sel, val_all = self._sync_checklists(
736                     opt = ctrl_panel.get("cl-ctrl-testtype-options"),
737                     sel=cl_testtype,
738                     all=list(),
739                     id=""
740                 )
741                 ctrl_panel.set({
742                     "cl-ctrl-testtype-value": val_sel,
743                     "cl-ctrl-testtype-all-value": val_all,
744                 })
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"),
748                     sel=list(),
749                     all=cl_testtype_all,
750                     id="all"
751                 )
752                 ctrl_panel.set({
753                     "cl-ctrl-testtype-value": val_sel,
754                     "cl-ctrl-testtype-all-value": val_all,
755                 })
756             elif trigger_id == "btn-ctrl-add":
757                 _ = btn_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:
767                         store_sel = list()
768                     for core in cores:
769                         for framesize in framesizes:
770                             for ttype in testtypes:
771                                 tid = (
772                                     f"{phy.replace('af_xdp', 'af-xdp')}-"
773                                     f"{area}-"
774                                     f"{framesize.lower()}-"
775                                     f"{core.lower()}-"
776                                     f"{test}-"
777                                     f"{ttype.lower()}"
778                                 )
779                                 if tid not in [itm["id"] for itm in store_sel]:
780                                     store_sel.append({
781                                         "id": tid,
782                                         "phy": phy,
783                                         "area": area,
784                                         "test": test,
785                                         "framesize": framesize.lower(),
786                                         "core": core.lower(),
787                                         "testtype": ttype.lower()
788                                     })
789                     ctrl_panel.set(ctrl_panel.defaults)
790                     ctrl_panel.set({
791                         "cl-selected-options": self._list_tests(store_sel)
792                     })
793             elif trigger_id in ("btn-sel-display", "dpr-period"):
794                 _ = btn_display
795                 fig_tput, fig_lat = graph_trending(
796                     self.data, store_sel, self.layout, d_start, d_end
797                 )
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":
801                 _ = btn_remove_all
802                 fig_tput = self.NO_GRAPH
803                 fig_lat = self.NO_GRAPH
804                 store_sel = list()
805                 ctrl_panel.set({
806                         "cl-selected-options": list()
807                 })
808             elif trigger_id == "btn-sel-remove":
809                 _ = btn_remove
810                 if list_sel:
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
816                 if store_sel:
817                     fig_tput, fig_lat = graph_trending(
818                         self.data, store_sel, self.layout, d_start, d_end
819                     )
820                     fig_tput = fig_tput if fig_tput else self.NO_GRAPH
821                     fig_lat = fig_lat if fig_lat else self.NO_GRAPH
822                     ctrl_panel.set({
823                         "cl-selected-options": self._list_tests(store_sel)
824                     })
825                 else:
826                     fig_tput = self.NO_GRAPH
827                     fig_lat = self.NO_GRAPH
828                     store_sel = list()
829                     ctrl_panel.set({
830                             "cl-selected-options": list()
831                     })
832
833             ret_val = [ctrl_panel.panel, store_sel, fig_tput, fig_lat]
834             ret_val.extend(ctrl_panel.values())
835             return ret_val
836
837         @app.callback(
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")
843         )
844         def _show_metadata_from_graphs(
845             tput_data: dict, lat_data: dict) -> tuple:
846             """
847             """
848             if not (tput_data or lat_data):
849                 raise PreventUpdate
850
851             metadata = no_update
852             graph = list()
853
854             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
855             if trigger_id == "graph-tput":
856                 title = "Throughput"
857                 txt = tput_data["points"][0]["text"].replace("<br>", "\n")
858             elif trigger_id == "graph-latency":
859                 title = "Latency"
860                 txt = lat_data["points"][0]["text"].replace("<br>", "\n")
861                 hdrh_data = lat_data["points"][0].get("customdata", None)
862                 if hdrh_data:
863                     graph = [dcc.Graph(
864                         id="hdrh-latency-graph",
865                         figure=graph_hdrh_latency(hdrh_data, self.layout)
866                     ), ]
867
868             metadata = [
869                 dbc.Card(
870                     children=[
871                         dbc.CardHeader(children=[
872                             dcc.Clipboard(
873                                 target_id="tput-lat-metadata",
874                                 title="Copy",
875                                 style={"display": "inline-block"}
876                             ),
877                             title
878                         ]),
879                         dbc.CardBody(
880                             id="tput-lat-metadata",
881                             children=[txt]
882                         )
883                     ]
884                 )
885             ]
886
887             return metadata, graph, True
888
889         @app.callback(
890             Output("download-data", "data"),
891             State("selected-tests", "data"),
892             Input("btn-download-data", "n_clicks"),
893             prevent_initial_call=True
894         )
895         def _download_data(store_sel, n_clicks):
896             """
897             """
898
899             if not n_clicks:
900                 raise PreventUpdate
901
902             df = pd.DataFrame()
903             for itm in store_sel:
904                 sel_data = select_trending_data(self.data, itm)
905                 if sel_data is None:
906                     continue
907                 df = pd.concat([df, sel_data], ignore_index=True)
908
909             return dcc.send_data_frame(df.to_csv, "trending_data.csv")