feat(uti): Form layout
[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",
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             ],
224         )
225
226     def _add_plotting_col(self) -> dbc.Col:
227         """Add column with plots and tables. It is placed on the right side.
228         """
229         return dbc.Col(
230             id="col-plotting-area",
231             children=[
232                 dbc.Row(  # Throughput
233                     id="row-graph-tput",
234                     class_name="g-0 p-2",
235                     children=[
236                         dcc.Loading(
237                             dcc.Graph(id="graph-tput")
238                         )
239                     ]
240                 ),
241                 dbc.Row(  # Latency
242                     id="row-graph-lat",
243                     class_name="g-0 p-2",
244                     children=[
245                         dcc.Loading(
246                             dcc.Graph(id="graph-latency")
247                         )
248                     ]
249                 ),
250                 dbc.Row(  # Download
251                     id="div-download",
252                     class_name="g-0",
253                     children=[
254                         dcc.Loading(children=[
255                             dbc.Button(
256                                 id="btn-download-data",
257                                 children=["Download Data"]
258                             ),
259                             dcc.Download(id="download-data")
260                         ])
261                     ]
262                 )
263             ],
264             width=9,
265         )
266
267     def _add_ctrl_panel(self) -> dbc.Row:
268         """
269         """
270         return dbc.Row(
271             id="row-ctrl-panel",
272             class_name="g-0 p-2",
273             children=[
274                 dbc.Row(
275                     class_name="gy-1",
276                     children=[
277                         dbc.Label(
278                             "Physical Test Bed Topology, NIC and Driver",
279                             class_name="p-0"
280                         ),
281                         dbc.Select(
282                             id="dd-ctrl-phy",
283                             placeholder="Select a Physical Test Bed Topology...",
284                             options=[
285                                 {"label": k, "value": k} for k in self.spec_tbs.keys()
286                             ],
287                             size="sm",
288                         ),
289                     ]
290                 ),
291                 dbc.Row(
292                     class_name="gy-1",
293                     children=[
294                         dbc.Label(
295                             "Area",
296                             class_name="p-0"
297                         ),
298                         dbc.Select(
299                             id="dd-ctrl-area",
300                             placeholder="Select an Area...",
301                             disabled=True,
302                             size="sm",
303                         ),
304                     ]
305                 ),
306                 dbc.Row(
307                     class_name="gy-1",
308                     children=[
309                         dbc.Label(
310                             "Test",
311                             class_name="p-0"
312                         ),
313                         dbc.Select(
314                             id="dd-ctrl-test",
315                             placeholder="Select a Test...",
316                             disabled=True,
317                             size="sm",
318                         ),
319                     ]
320                 ),
321                 dbc.Row(
322                     id="row-ctrl-core",
323                     class_name="gy-1",
324                     children=[
325                         dbc.Label(
326                             "Number of Cores",
327                             class_name="p-0"
328                         ),
329                         dbc.Col(
330                             children=[
331                                 dbc.Checklist(
332                                     id="cl-ctrl-core-all",
333                                     options=self.CL_ALL_DISABLED,
334                                     inline=False,
335                                     switch=False
336                                 )
337                             ],
338                             width=3
339                         ),
340                         dbc.Col(
341                             children=[
342                                 dbc.Checklist(
343                                     id="cl-ctrl-core",
344                                     inline=True,
345                                     switch=False
346                                 )
347                             ]
348                         )
349                     ]
350                 ),
351                 dbc.Row(
352                     id="row-ctrl-framesize",
353                     class_name="gy-1",
354                     children=[
355                         dbc.Label(
356                             "Frame Size",
357                             class_name="p-0"
358                         ),
359                         dbc.Col(
360                             children=[
361                                 dbc.Checklist(
362                                     id="cl-ctrl-framesize-all",
363                                     options=self.CL_ALL_DISABLED,
364                                     inline=True,
365                                     switch=False
366                                 ),
367                             ],
368                             width=3
369                         ),
370                         dbc.Col(
371                             children=[
372                                 dbc.Checklist(
373                                     id="cl-ctrl-framesize",
374                                     inline=True,
375                                     switch=False
376                                 )
377                             ]
378                         )
379                     ]
380                 ),
381                 dbc.Row(
382                     id="row-ctrl-testtype",
383                     class_name="gy-1",
384                     children=[
385                         dbc.Label(
386                             "Test Type",
387                             class_name="p-0"
388                         ),
389                         dbc.Col(
390                             children=[
391                                 dbc.Checklist(
392                                     id="cl-ctrl-testtype-all",
393                                     options=self.CL_ALL_DISABLED,
394                                     inline=True,
395                                     switch=False
396                                 ),
397                             ],
398                             width=3
399                         ),
400                         dbc.Col(
401                             children=[
402                                 dbc.Checklist(
403                                     id="cl-ctrl-testtype",
404                                     inline=True,
405                                     switch=False
406                                 )
407                             ]
408                         )
409                     ]
410                 ),
411                 dbc.Row(
412                     class_name="gy-1",
413                     children=[
414                         dbc.ButtonGroup(
415                             [
416                                 dbc.Button(
417                                     id="btn-ctrl-add",
418                                     children="Add Selected",
419                                     color="secondary",
420                                 )
421                             ],
422                             size="md",
423                         )
424                     ]
425                 ),
426                 dbc.Row(
427                     class_name="gy-1",
428                     children=[
429                         dcc.DatePickerRange(
430                             id="dpr-period",
431                             min_date_allowed=\
432                                 datetime.utcnow()-timedelta(days=180),
433                             max_date_allowed=datetime.utcnow(),
434                             initial_visible_month=datetime.utcnow(),
435                             start_date=datetime.utcnow() - timedelta(days=180),
436                             end_date=datetime.utcnow(),
437                             display_format="D MMMM YY"
438                         )
439                     ]
440                 ),
441                 dbc.Row(
442                     class_name="gy-1",
443                     children=[
444                         dbc.Card(
445                             class_name="p-0",
446                             children=[
447                                 dbc.Label(
448                                     "Selected tests",
449                                     class_name="p-0"
450                                 ),
451                                 dbc.Checklist(
452                                     id="cl-selected",
453                                     options=[],
454                                     inline=False
455                                 )
456                             ],
457                             color="light",
458                             outline=True
459                         )
460                     ]
461                 ),
462                 dbc.Row(
463                     children=[
464                         dbc.ButtonGroup(
465                             [
466                                 dbc.Button(
467                                     id="btn-sel-remove-all",
468                                     children="Remove All",
469                                     class_name="w-100",
470                                     color="secondary",
471                                     disabled=False
472                                 ),
473                                 dbc.Button(
474                                     id="btn-sel-remove",
475                                     children="Remove Selected",
476                                     class_name="w-100",
477                                     color="secondary",
478                                     disabled=False
479                                 ),
480                                 dbc.Button(
481                                     id="btn-sel-display",
482                                     children="Display",
483                                     class_name="w-100",
484                                     color="secondary",
485                                     disabled=False
486                                 )
487                             ],
488                             size="md",
489                         ),
490                     ]
491                 ),
492             ]
493         )
494
495     class ControlPanel:
496         def __init__(self, panel: dict) -> None:
497
498             CL_ALL_DISABLED = [{
499                 "label": "All",
500                 "value": "all",
501                 "disabled": True
502             }]
503
504             # Defines also the order of keys
505             self._defaults = {
506                 "dd-ctrl-phy-value": str(),
507                 "dd-ctrl-area-options": list(),
508                 "dd-ctrl-area-disabled": True,
509                 "dd-ctrl-area-value": str(),
510                 "dd-ctrl-test-options": list(),
511                 "dd-ctrl-test-disabled": True,
512                 "dd-ctrl-test-value": str(),
513                 "cl-ctrl-core-options": list(),
514                 "cl-ctrl-core-value": list(),
515                 "cl-ctrl-core-all-value": list(),
516                 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
517                 "cl-ctrl-framesize-options": list(),
518                 "cl-ctrl-framesize-value": list(),
519                 "cl-ctrl-framesize-all-value": list(),
520                 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
521                 "cl-ctrl-testtype-options": list(),
522                 "cl-ctrl-testtype-value": list(),
523                 "cl-ctrl-testtype-all-value": list(),
524                 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
525                 "btn-ctrl-add-disabled": True,
526                 "cl-selected-options": list(),
527             }
528
529             self._panel = deepcopy(self._defaults)
530             if panel:
531                 for key in self._defaults:
532                     self._panel[key] = panel[key]
533
534         @property
535         def defaults(self) -> dict:
536             return self._defaults
537
538         @property
539         def panel(self) -> dict:
540             return self._panel
541
542         def set(self, kwargs: dict) -> None:
543             for key, val in kwargs.items():
544                 if key in self._panel:
545                     self._panel[key] = val
546                 else:
547                     raise KeyError(f"The key {key} is not defined.")
548
549         def get(self, key: str) -> any:
550             return self._panel[key]
551
552         def values(self) -> tuple:
553             return tuple(self._panel.values())
554
555     @staticmethod
556     def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
557         """
558         """
559         options = {v["value"] for v in opt}
560         if id =="all":
561             sel = list(options) if all else list()
562         else:
563             all = ["all", ] if set(sel) == options else list()
564         return sel, all
565
566     @staticmethod
567     def _list_tests(selection: dict) -> list:
568         """Display selected tests with checkboxes
569         """
570         if selection:
571             return [
572                 {"label": v["id"], "value": v["id"]} for v in selection
573             ]
574         else:
575             return list()
576
577     def callbacks(self, app):
578
579         @app.callback(
580             Output("control-panel", "data"),  # Store
581             Output("selected-tests", "data"),  # Store
582             Output("graph-tput", "figure"),
583             Output("graph-latency", "figure"),
584             Output("dd-ctrl-phy", "value"),
585             Output("dd-ctrl-area", "options"),
586             Output("dd-ctrl-area", "disabled"),
587             Output("dd-ctrl-area", "value"),
588             Output("dd-ctrl-test", "options"),
589             Output("dd-ctrl-test", "disabled"),
590             Output("dd-ctrl-test", "value"),
591             Output("cl-ctrl-core", "options"),
592             Output("cl-ctrl-core", "value"),
593             Output("cl-ctrl-core-all", "value"),
594             Output("cl-ctrl-core-all", "options"),
595             Output("cl-ctrl-framesize", "options"),
596             Output("cl-ctrl-framesize", "value"),
597             Output("cl-ctrl-framesize-all", "value"),
598             Output("cl-ctrl-framesize-all", "options"),
599             Output("cl-ctrl-testtype", "options"),
600             Output("cl-ctrl-testtype", "value"),
601             Output("cl-ctrl-testtype-all", "value"),
602             Output("cl-ctrl-testtype-all", "options"),
603             Output("btn-ctrl-add", "disabled"),
604             Output("cl-selected", "options"),  # User selection
605             State("control-panel", "data"),  # Store
606             State("selected-tests", "data"),  # Store
607             State("cl-selected", "value"),  # User selection
608             Input("dd-ctrl-phy", "value"),
609             Input("dd-ctrl-area", "value"),
610             Input("dd-ctrl-test", "value"),
611             Input("cl-ctrl-core", "value"),
612             Input("cl-ctrl-core-all", "value"),
613             Input("cl-ctrl-framesize", "value"),
614             Input("cl-ctrl-framesize-all", "value"),
615             Input("cl-ctrl-testtype", "value"),
616             Input("cl-ctrl-testtype-all", "value"),
617             Input("btn-ctrl-add", "n_clicks"),
618             Input("dpr-period", "start_date"),
619             Input("dpr-period", "end_date"),
620             Input("btn-sel-display", "n_clicks"),
621             Input("btn-sel-remove", "n_clicks"),
622             Input("btn-sel-remove-all", "n_clicks"),
623         )
624         def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
625             dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
626             cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
627             cl_testtype: list, cl_testtype_all: list, btn_add: int,
628             d_start: str, d_end: str, btn_display: int, btn_remove: int,
629             btn_remove_all: int) -> tuple:
630             """
631             """
632
633             d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
634                 int(d_start[8:10]))
635             d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
636
637             fig_tput = no_update
638             fig_lat = no_update
639
640             ctrl_panel = self.ControlPanel(cp_data)
641
642             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
643
644             if trigger_id == "dd-ctrl-phy":
645                 try:
646                     options = [
647                         {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
648                             for v in [v for v in self.spec_tbs[dd_phy].keys()]
649                     ]
650                     disabled = False
651                 except KeyError:
652                     options = list()
653                     disabled = no_update
654                 ctrl_panel.set({
655                     "dd-ctrl-phy-value": dd_phy,
656                     "dd-ctrl-area-value": str(),
657                     "dd-ctrl-area-options": options,
658                     "dd-ctrl-area-disabled": disabled,
659                     "dd-ctrl-test-options": list(),
660                     "dd-ctrl-test-disabled": True,
661                     "cl-ctrl-core-options": list(),
662                     "cl-ctrl-core-value": list(),
663                     "cl-ctrl-core-all-value": list(),
664                     "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
665                     "cl-ctrl-framesize-options": list(),
666                     "cl-ctrl-framesize-value": list(),
667                     "cl-ctrl-framesize-all-value": list(),
668                     "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
669                     "cl-ctrl-testtype-options": list(),
670                     "cl-ctrl-testtype-value": list(),
671                     "cl-ctrl-testtype-all-value": list(),
672                     "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
673                     "btn-ctrl-add-disabled": True,
674                 })
675             elif trigger_id == "dd-ctrl-area":
676                 try:
677                     phy = ctrl_panel.get("dd-ctrl-phy-value")
678                     options = [
679                         {"label": v, "value": v}
680                             for v in self.spec_tbs[phy][dd_area]["test"]
681                     ]
682                     disabled = False
683                 except KeyError:
684                     options = list()
685                     disabled = True
686                 ctrl_panel.set({
687                     "dd-ctrl-area-value": dd_area,
688                     "dd-ctrl-test-value": str(),
689                     "dd-ctrl-test-options": options,
690                     "dd-ctrl-test-disabled": disabled,
691                     "cl-ctrl-core-options": list(),
692                     "cl-ctrl-core-value": list(),
693                     "cl-ctrl-core-all-value": list(),
694                     "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
695                     "cl-ctrl-framesize-options": list(),
696                     "cl-ctrl-framesize-value": list(),
697                     "cl-ctrl-framesize-all-value": list(),
698                     "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
699                     "cl-ctrl-testtype-options": list(),
700                     "cl-ctrl-testtype-value": list(),
701                     "cl-ctrl-testtype-all-value": list(),
702                     "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
703                     "btn-ctrl-add-disabled": True,
704                 })
705             elif trigger_id == "dd-ctrl-test":
706                 core_opts = list()
707                 framesize_opts = list()
708                 testtype_opts = list()
709                 phy = ctrl_panel.get("dd-ctrl-phy-value")
710                 area = ctrl_panel.get("dd-ctrl-area-value")
711                 if phy and area and dd_test:
712                     core_opts = [
713                         {"label": v, "value": v}
714                             for v in self.spec_tbs[phy][area]["core"]
715                     ]
716                     framesize_opts = [
717                         {"label": v, "value": v}
718                             for v in self.spec_tbs[phy][area]["frame-size"]
719                     ]
720                     testtype_opts = [
721                         {"label": v, "value": v}
722                             for v in self.spec_tbs[phy][area]["test-type"]
723                     ]
724                     ctrl_panel.set({
725                         "dd-ctrl-test-value": dd_test,
726                         "cl-ctrl-core-options": core_opts,
727                         "cl-ctrl-core-value": list(),
728                         "cl-ctrl-core-all-value": list(),
729                         "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
730                         "cl-ctrl-framesize-options": framesize_opts,
731                         "cl-ctrl-framesize-value": list(),
732                         "cl-ctrl-framesize-all-value": list(),
733                         "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
734                         "cl-ctrl-testtype-options": testtype_opts,
735                         "cl-ctrl-testtype-value": list(),
736                         "cl-ctrl-testtype-all-value": list(),
737                         "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
738                         "btn-ctrl-add-disabled": False,
739                     })
740             elif trigger_id == "cl-ctrl-core":
741                 val_sel, val_all = self._sync_checklists(
742                     opt=ctrl_panel.get("cl-ctrl-core-options"),
743                     sel=cl_core,
744                     all=list(),
745                     id=""
746                 )
747                 ctrl_panel.set({
748                     "cl-ctrl-core-value": val_sel,
749                     "cl-ctrl-core-all-value": val_all,
750                 })
751             elif trigger_id == "cl-ctrl-core-all":
752                 val_sel, val_all = self._sync_checklists(
753                     opt = ctrl_panel.get("cl-ctrl-core-options"),
754                     sel=list(),
755                     all=cl_core_all,
756                     id="all"
757                 )
758                 ctrl_panel.set({
759                     "cl-ctrl-core-value": val_sel,
760                     "cl-ctrl-core-all-value": val_all,
761                 })
762             elif trigger_id == "cl-ctrl-framesize":
763                 val_sel, val_all = self._sync_checklists(
764                     opt = ctrl_panel.get("cl-ctrl-framesize-options"),
765                     sel=cl_framesize,
766                     all=list(),
767                     id=""
768                 )
769                 ctrl_panel.set({
770                     "cl-ctrl-framesize-value": val_sel,
771                     "cl-ctrl-framesize-all-value": val_all,
772                 })
773             elif trigger_id == "cl-ctrl-framesize-all":
774                 val_sel, val_all = self._sync_checklists(
775                     opt = ctrl_panel.get("cl-ctrl-framesize-options"),
776                     sel=list(),
777                     all=cl_framesize_all,
778                     id="all"
779                 )
780                 ctrl_panel.set({
781                     "cl-ctrl-framesize-value": val_sel,
782                     "cl-ctrl-framesize-all-value": val_all,
783                 })
784             elif trigger_id == "cl-ctrl-testtype":
785                 val_sel, val_all = self._sync_checklists(
786                     opt = ctrl_panel.get("cl-ctrl-testtype-options"),
787                     sel=cl_testtype,
788                     all=list(),
789                     id=""
790                 )
791                 ctrl_panel.set({
792                     "cl-ctrl-testtype-value": val_sel,
793                     "cl-ctrl-testtype-all-value": val_all,
794                 })
795             elif trigger_id == "cl-ctrl-testtype-all":
796                 val_sel, val_all = self._sync_checklists(
797                     opt = ctrl_panel.get("cl-ctrl-testtype-options"),
798                     sel=list(),
799                     all=cl_testtype_all,
800                     id="all"
801                 )
802                 ctrl_panel.set({
803                     "cl-ctrl-testtype-value": val_sel,
804                     "cl-ctrl-testtype-all-value": val_all,
805                 })
806             elif trigger_id == "btn-ctrl-add":
807                 _ = btn_add
808                 phy = ctrl_panel.get("dd-ctrl-phy-value")
809                 area = ctrl_panel.get("dd-ctrl-area-value")
810                 test = ctrl_panel.get("dd-ctrl-test-value")
811                 cores = ctrl_panel.get("cl-ctrl-core-value")
812                 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
813                 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
814                 # Add selected test to the list of tests in store:
815                 if phy and area and test and cores and framesizes and testtypes:
816                     if store_sel is None:
817                         store_sel = list()
818                     for core in cores:
819                         for framesize in framesizes:
820                             for ttype in testtypes:
821                                 tid = (
822                                     f"{phy.replace('af_xdp', 'af-xdp')}-"
823                                     f"{area}-"
824                                     f"{framesize.lower()}-"
825                                     f"{core.lower()}-"
826                                     f"{test}-"
827                                     f"{ttype.lower()}"
828                                 )
829                                 if tid not in [itm["id"] for itm in store_sel]:
830                                     store_sel.append({
831                                         "id": tid,
832                                         "phy": phy,
833                                         "area": area,
834                                         "test": test,
835                                         "framesize": framesize.lower(),
836                                         "core": core.lower(),
837                                         "testtype": ttype.lower()
838                                     })
839                     ctrl_panel.set(ctrl_panel.defaults)
840                     ctrl_panel.set({
841                         "cl-selected-options": self._list_tests(store_sel)
842                     })
843             elif trigger_id in ("btn-sel-display", "dpr-period"):
844                 _ = btn_display
845                 fig_tput, fig_lat = graph_trending(
846                     self.data, store_sel, self.layout, d_start, d_end
847                 )
848                 fig_tput = fig_tput if fig_tput else self.NO_GRAPH
849                 fig_lat = fig_lat if fig_lat else self.NO_GRAPH
850             elif trigger_id == "btn-sel-remove-all":
851                 _ = btn_remove_all
852                 fig_tput = self.NO_GRAPH
853                 fig_lat = self.NO_GRAPH
854                 store_sel = list()
855                 ctrl_panel.set({
856                         "cl-selected-options": list()
857                 })
858             elif trigger_id == "btn-sel-remove":
859                 _ = btn_remove
860                 if list_sel:
861                     new_store_sel = list()
862                     for item in store_sel:
863                         if item["id"] not in list_sel:
864                             new_store_sel.append(item)
865                     store_sel = new_store_sel
866                 if store_sel:
867                     fig_tput, fig_lat = graph_trending(
868                         self.data, store_sel, self.layout, d_start, d_end
869                     )
870                     fig_tput = fig_tput if fig_tput else self.NO_GRAPH
871                     fig_lat = fig_lat if fig_lat else self.NO_GRAPH
872                     ctrl_panel.set({
873                         "cl-selected-options": self._list_tests(store_sel)
874                     })
875                 else:
876                     fig_tput = self.NO_GRAPH
877                     fig_lat = self.NO_GRAPH
878                     store_sel = list()
879                     ctrl_panel.set({
880                             "cl-selected-options": list()
881                     })
882
883             ret_val = [ctrl_panel.panel, store_sel, fig_tput, fig_lat]
884             ret_val.extend(ctrl_panel.values())
885             return ret_val
886
887         @app.callback(
888             Output("metadata-tput-lat", "children"),
889             Output("metadata-hdrh-graph", "children"),
890             Output("offcanvas-metadata", "is_open"),
891             Input("graph-tput", "clickData"),
892             Input("graph-latency", "clickData")
893         )
894         def _show_metadata_from_graphs(
895             tput_data: dict, lat_data: dict) -> tuple:
896             """
897             """
898             if not (tput_data or lat_data):
899                 raise PreventUpdate
900
901             metadata = no_update
902             graph = list()
903
904             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
905             if trigger_id == "graph-tput":
906                 title = "Throughput"
907                 txt = tput_data["points"][0]["text"].replace("<br>", "\n")
908             elif trigger_id == "graph-latency":
909                 title = "Latency"
910                 txt = lat_data["points"][0]["text"].replace("<br>", "\n")
911                 hdrh_data = lat_data["points"][0].get("customdata", None)
912                 if hdrh_data:
913                     graph = [dcc.Graph(
914                         id="hdrh-latency-graph",
915                         figure=graph_hdrh_latency(hdrh_data, self.layout)
916                     ), ]
917
918             metadata = [
919                 dbc.Card(
920                     class_name="g-0",
921                     children=[
922                         dbc.CardHeader(children=[
923                             dcc.Clipboard(
924                                 target_id="tput-lat-metadata",
925                                 title="Copy",
926                                 style={"display": "inline-block"}
927                             ),
928                             title
929                         ]),
930                         dbc.CardBody(
931                             id="tput-lat-metadata",
932                             children=[txt]
933                         )
934                     ]
935                 )
936             ]
937
938             return metadata, graph, True
939
940         @app.callback(
941             Output("download-data", "data"),
942             State("selected-tests", "data"),
943             Input("btn-download-data", "n_clicks"),
944             prevent_initial_call=True
945         )
946         def _download_data(store_sel, n_clicks):
947             """
948             """
949
950             if not n_clicks:
951                 raise PreventUpdate
952
953             df = pd.DataFrame()
954             for itm in store_sel:
955                 sel_data = select_trending_data(self.data, itm)
956                 if sel_data is None:
957                     continue
958                 df = pd.concat([df, sel_data], ignore_index=True)
959
960             return dcc.send_data_frame(df.to_csv, "trending_data.csv")