feat(uti): Remove button "Display"
[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     STYLE_DISABLED = {"display": "none"}
39     STYLE_ENABLED = {"display": "inherit"}
40
41     CL_ALL_DISABLED = [{
42         "label": "All",
43         "value": "all",
44         "disabled": True
45     }]
46     CL_ALL_ENABLED = [{
47         "label": "All",
48         "value": "all",
49         "disabled": False
50     }]
51
52     PLACEHOLDER = html.Nobr("")
53
54     def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
55         data_spec_file):
56         """
57         """
58
59         # Inputs
60         self._app = app
61         self._html_layout_file = html_layout_file
62         self._spec_file = spec_file
63         self._graph_layout_file = graph_layout_file
64         self._data_spec_file = data_spec_file
65
66         # Read the data:
67         data_mrr = Data(
68             data_spec_file=self._data_spec_file,
69             debug=True
70         ).read_trending_mrr(days=5)
71
72         data_ndrpdr = Data(
73             data_spec_file=self._data_spec_file,
74             debug=True
75         ).read_trending_ndrpdr(days=14)
76
77         self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
78
79         # Read from files:
80         self._html_layout = ""
81         self._spec_tbs = None
82         self._graph_layout = None
83
84         try:
85             with open(self._html_layout_file, "r") as file_read:
86                 self._html_layout = file_read.read()
87         except IOError as err:
88             raise RuntimeError(
89                 f"Not possible to open the file {self._html_layout_file}\n{err}"
90             )
91
92         try:
93             with open(self._spec_file, "r") as file_read:
94                 self._spec_tbs = load(file_read, Loader=FullLoader)
95         except IOError as err:
96             raise RuntimeError(
97                 f"Not possible to open the file {self._spec_file,}\n{err}"
98             )
99         except YAMLError as err:
100             raise RuntimeError(
101                 f"An error occurred while parsing the specification file "
102                 f"{self._spec_file,}\n"
103                 f"{err}"
104             )
105
106         try:
107             with open(self._graph_layout_file, "r") as file_read:
108                 self._graph_layout = load(file_read, Loader=FullLoader)
109         except IOError as err:
110             raise RuntimeError(
111                 f"Not possible to open the file {self._graph_layout_file}\n"
112                 f"{err}"
113             )
114         except YAMLError as err:
115             raise RuntimeError(
116                 f"An error occurred while parsing the specification file "
117                 f"{self._graph_layout_file}\n"
118                 f"{err}"
119             )
120
121         # Callbacks:
122         if self._app is not None and hasattr(self, 'callbacks'):
123             self.callbacks(self._app)
124
125     @property
126     def html_layout(self):
127         return self._html_layout
128
129     @property
130     def spec_tbs(self):
131         return self._spec_tbs
132
133     @property
134     def data(self):
135         return self._data
136
137     @property
138     def layout(self):
139         return self._graph_layout
140
141     def add_content(self):
142         """
143         """
144         if self.html_layout and self.spec_tbs:
145             return html.Div(
146                 id="div-main",
147                 children=[
148                     dbc.Row(
149                         id="row-navbar",
150                         class_name="g-0",
151                         children=[
152                             self._add_navbar(),
153                         ]
154                     ),
155                     dcc.Loading(
156                         dbc.Offcanvas(
157                             class_name="w-50",
158                             id="offcanvas-metadata",
159                             title="Throughput And Latency",
160                             placement="end",
161                             is_open=False,
162                             children=[
163                                 dbc.Row(id="metadata-tput-lat"),
164                                 dbc.Row(id="metadata-hdrh-graph"),
165                             ]
166                         )
167                     ),
168                     dbc.Row(
169                         id="row-main",
170                         class_name="g-0",
171                         children=[
172                             dcc.Store(
173                                 id="selected-tests"
174                             ),
175                             dcc.Store(
176                                 id="control-panel"
177                             ),
178                             self._add_ctrl_col(),
179                             self._add_plotting_col(),
180                         ]
181                     )
182                 ]
183             )
184         else:
185             return html.Div(
186                 id="div-main-error",
187                 children=[
188                     dbc.Alert(
189                         [
190                             "An Error Occured",
191                         ],
192                         color="danger",
193                     ),
194                 ]
195             )
196
197     def _add_navbar(self):
198         """Add nav element with navigation panel. It is placed on the top.
199         """
200         return dbc.NavbarSimple(
201             id="navbarsimple-main",
202             children=[
203                 dbc.NavItem(
204                     dbc.NavLink(
205                         "Continuous Performance Trending",
206                         disabled=True,
207                         external_link=True,
208                         href="#"
209                     )
210                 )
211             ],
212             brand="Dashboard",
213             brand_href="/",
214             brand_external_link=True,
215             class_name="p-2",
216             fluid=True,
217         )
218
219     def _add_ctrl_col(self) -> dbc.Col:
220         """Add column with controls. It is placed on the left side.
221         """
222         return dbc.Col(
223             id="col-controls",
224             children=[
225                 self._add_ctrl_panel(),
226             ],
227         )
228
229     def _add_plotting_col(self) -> dbc.Col:
230         """Add column with plots and tables. It is placed on the right side.
231         """
232         return dbc.Col(
233             id="col-plotting-area",
234             children=[
235                 dbc.Row(  # Throughput
236                     id="row-graph-tput",
237                     class_name="g-0 p-2",
238                     children=[
239                         self.PLACEHOLDER
240                     ]
241                 ),
242                 dbc.Row(  # Latency
243                     id="row-graph-lat",
244                     class_name="g-0 p-2",
245                     children=[
246                         self.PLACEHOLDER
247                     ]
248                 ),
249                 dbc.Row(  # Download
250                     id="row-btn-download",
251                     class_name="g-0 p-2",
252                     children=[
253                         self.PLACEHOLDER
254                     ]
255                 )
256             ],
257             width=9,
258         )
259
260     def _add_ctrl_panel(self) -> dbc.Row:
261         """
262         """
263         return dbc.Row(
264             id="row-ctrl-panel",
265             class_name="g-0 p-2",
266             children=[
267                 dbc.Row(
268                     class_name="gy-1",
269                     children=[
270                         dbc.Label(
271                             "Physical Test Bed Topology, NIC and Driver",
272                             class_name="p-0"
273                         ),
274                         dbc.Select(
275                             id="dd-ctrl-phy",
276                             placeholder="Select a Physical Test Bed Topology...",
277                             options=[
278                                 {"label": k, "value": k} for k in self.spec_tbs.keys()
279                             ],
280                             size="sm",
281                         ),
282                     ]
283                 ),
284                 dbc.Row(
285                     class_name="gy-1",
286                     children=[
287                         dbc.Label(
288                             "Area",
289                             class_name="p-0"
290                         ),
291                         dbc.Select(
292                             id="dd-ctrl-area",
293                             placeholder="Select an Area...",
294                             disabled=True,
295                             size="sm",
296                         ),
297                     ]
298                 ),
299                 dbc.Row(
300                     class_name="gy-1",
301                     children=[
302                         dbc.Label(
303                             "Test",
304                             class_name="p-0"
305                         ),
306                         dbc.Select(
307                             id="dd-ctrl-test",
308                             placeholder="Select a Test...",
309                             disabled=True,
310                             size="sm",
311                         ),
312                     ]
313                 ),
314                 dbc.Row(
315                     id="row-ctrl-core",
316                     class_name="gy-1",
317                     children=[
318                         dbc.Label(
319                             "Number of Cores",
320                             class_name="p-0"
321                         ),
322                         dbc.Col(
323                             children=[
324                                 dbc.Checklist(
325                                     id="cl-ctrl-core-all",
326                                     options=self.CL_ALL_DISABLED,
327                                     inline=False,
328                                     switch=False
329                                 )
330                             ],
331                             width=3
332                         ),
333                         dbc.Col(
334                             children=[
335                                 dbc.Checklist(
336                                     id="cl-ctrl-core",
337                                     inline=True,
338                                     switch=False
339                                 )
340                             ]
341                         )
342                     ]
343                 ),
344                 dbc.Row(
345                     id="row-ctrl-framesize",
346                     class_name="gy-1",
347                     children=[
348                         dbc.Label(
349                             "Frame Size",
350                             class_name="p-0"
351                         ),
352                         dbc.Col(
353                             children=[
354                                 dbc.Checklist(
355                                     id="cl-ctrl-framesize-all",
356                                     options=self.CL_ALL_DISABLED,
357                                     inline=True,
358                                     switch=False
359                                 ),
360                             ],
361                             width=3
362                         ),
363                         dbc.Col(
364                             children=[
365                                 dbc.Checklist(
366                                     id="cl-ctrl-framesize",
367                                     inline=True,
368                                     switch=False
369                                 )
370                             ]
371                         )
372                     ]
373                 ),
374                 dbc.Row(
375                     id="row-ctrl-testtype",
376                     class_name="gy-1",
377                     children=[
378                         dbc.Label(
379                             "Test Type",
380                             class_name="p-0"
381                         ),
382                         dbc.Col(
383                             children=[
384                                 dbc.Checklist(
385                                     id="cl-ctrl-testtype-all",
386                                     options=self.CL_ALL_DISABLED,
387                                     inline=True,
388                                     switch=False
389                                 ),
390                             ],
391                             width=3
392                         ),
393                         dbc.Col(
394                             children=[
395                                 dbc.Checklist(
396                                     id="cl-ctrl-testtype",
397                                     inline=True,
398                                     switch=False
399                                 )
400                             ]
401                         )
402                     ]
403                 ),
404                 dbc.Row(
405                     class_name="gy-1",
406                     children=[
407                         dbc.ButtonGroup(
408                             [
409                                 dbc.Button(
410                                     id="btn-ctrl-add",
411                                     children="Add Selected",
412                                     color="secondary",
413                                 )
414                             ],
415                             size="md",
416                         )
417                     ]
418                 ),
419                 dbc.Row(
420                     class_name="gy-1",
421                     children=[
422                         dcc.DatePickerRange(
423                             id="dpr-period",
424                             className="d-flex justify-content-center",
425                             min_date_allowed=\
426                                 datetime.utcnow()-timedelta(days=180),
427                             max_date_allowed=datetime.utcnow(),
428                             initial_visible_month=datetime.utcnow(),
429                             start_date=datetime.utcnow() - timedelta(days=180),
430                             end_date=datetime.utcnow(),
431                             display_format="D MMMM YY"
432                         )
433                     ]
434                 ),
435                 dbc.Row(
436                     id="row-card-sel-tests",
437                     class_name="gy-1",
438                     style=self.STYLE_DISABLED,
439                     children=[
440                         dbc.Label(
441                             "Selected tests",
442                             class_name="p-0"
443                         ),
444                         dbc.Checklist(
445                             class_name="overflow-auto",
446                             id="cl-selected",
447                             options=[],
448                             inline=False,
449                             style={"max-height": "12em"},
450                         )
451                     ],
452                 ),
453                 dbc.Row(
454                     id="row-btns-sel-tests",
455                     style=self.STYLE_DISABLED,
456                     children=[
457                         dbc.ButtonGroup(
458                             children=[
459                                 dbc.Button(
460                                     id="btn-sel-remove-all",
461                                     children="Remove All",
462                                     class_name="w-100",
463                                     color="secondary",
464                                     disabled=False
465                                 ),
466                                 dbc.Button(
467                                     id="btn-sel-remove",
468                                     children="Remove Selected",
469                                     class_name="w-100",
470                                     color="secondary",
471                                     disabled=False
472                                 ),
473                             ],
474                             size="md",
475                         )
476                     ]
477                 ),
478             ]
479         )
480
481     class ControlPanel:
482         def __init__(self, panel: dict) -> None:
483
484             CL_ALL_DISABLED = [{
485                 "label": "All",
486                 "value": "all",
487                 "disabled": True
488             }]
489
490             # Defines also the order of keys
491             self._defaults = {
492                 "dd-ctrl-phy-value": str(),
493                 "dd-ctrl-area-options": list(),
494                 "dd-ctrl-area-disabled": True,
495                 "dd-ctrl-area-value": str(),
496                 "dd-ctrl-test-options": list(),
497                 "dd-ctrl-test-disabled": True,
498                 "dd-ctrl-test-value": str(),
499                 "cl-ctrl-core-options": list(),
500                 "cl-ctrl-core-value": list(),
501                 "cl-ctrl-core-all-value": list(),
502                 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
503                 "cl-ctrl-framesize-options": list(),
504                 "cl-ctrl-framesize-value": list(),
505                 "cl-ctrl-framesize-all-value": list(),
506                 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
507                 "cl-ctrl-testtype-options": list(),
508                 "cl-ctrl-testtype-value": list(),
509                 "cl-ctrl-testtype-all-value": list(),
510                 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
511                 "btn-ctrl-add-disabled": True,
512                 "cl-selected-options": list(),
513             }
514
515             self._panel = deepcopy(self._defaults)
516             if panel:
517                 for key in self._defaults:
518                     self._panel[key] = panel[key]
519
520         @property
521         def defaults(self) -> dict:
522             return self._defaults
523
524         @property
525         def panel(self) -> dict:
526             return self._panel
527
528         def set(self, kwargs: dict) -> None:
529             for key, val in kwargs.items():
530                 if key in self._panel:
531                     self._panel[key] = val
532                 else:
533                     raise KeyError(f"The key {key} is not defined.")
534
535         def get(self, key: str) -> any:
536             return self._panel[key]
537
538         def values(self) -> tuple:
539             return tuple(self._panel.values())
540
541     @staticmethod
542     def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
543         """
544         """
545         options = {v["value"] for v in opt}
546         if id =="all":
547             sel = list(options) if all else list()
548         else:
549             all = ["all", ] if set(sel) == options else list()
550         return sel, all
551
552     @staticmethod
553     def _list_tests(selection: dict) -> list:
554         """Display selected tests with checkboxes
555         """
556         if selection:
557             return [
558                 {"label": v["id"], "value": v["id"]} for v in selection
559             ]
560         else:
561             return list()
562
563     def callbacks(self, app):
564
565         def _generate_plotting_arrea(args: tuple) -> tuple:
566             """
567             """
568
569             (fig_tput, fig_lat) = args
570
571             row_fig_tput = self.PLACEHOLDER
572             row_fig_lat = self.PLACEHOLDER
573             row_btn_dwnld = self.PLACEHOLDER
574
575             if fig_tput:
576                 row_fig_tput = [
577                     dcc.Loading(
578                         dcc.Graph(
579                             id="graph-tput",
580                             figure=fig_tput
581                         )
582                     ),
583                 ]
584                 row_btn_dwnld = [
585                     dcc.Loading(children=[
586                         dbc.Button(
587                             id="btn-download-data",
588                             children=["Download Data"]
589                         ),
590                         dcc.Download(id="download-data")
591                     ]),
592                 ]
593             if fig_lat:
594                 row_fig_lat = [
595                     dcc.Loading(
596                         dcc.Graph(
597                             id="graph-latency",
598                             figure=fig_lat
599                         )
600                     )
601                 ]
602
603             return row_fig_tput, row_fig_lat, row_btn_dwnld
604
605         @app.callback(
606             Output("control-panel", "data"),  # Store
607             Output("selected-tests", "data"),  # Store
608             Output("row-graph-tput", "children"),
609             Output("row-graph-lat", "children"),
610             Output("row-btn-download", "children"),
611             Output("row-card-sel-tests", "style"),
612             Output("row-btns-sel-tests", "style"),
613             Output("dd-ctrl-phy", "value"),
614             Output("dd-ctrl-area", "options"),
615             Output("dd-ctrl-area", "disabled"),
616             Output("dd-ctrl-area", "value"),
617             Output("dd-ctrl-test", "options"),
618             Output("dd-ctrl-test", "disabled"),
619             Output("dd-ctrl-test", "value"),
620             Output("cl-ctrl-core", "options"),
621             Output("cl-ctrl-core", "value"),
622             Output("cl-ctrl-core-all", "value"),
623             Output("cl-ctrl-core-all", "options"),
624             Output("cl-ctrl-framesize", "options"),
625             Output("cl-ctrl-framesize", "value"),
626             Output("cl-ctrl-framesize-all", "value"),
627             Output("cl-ctrl-framesize-all", "options"),
628             Output("cl-ctrl-testtype", "options"),
629             Output("cl-ctrl-testtype", "value"),
630             Output("cl-ctrl-testtype-all", "value"),
631             Output("cl-ctrl-testtype-all", "options"),
632             Output("btn-ctrl-add", "disabled"),
633             Output("cl-selected", "options"),  # User selection
634             State("control-panel", "data"),  # Store
635             State("selected-tests", "data"),  # Store
636             State("cl-selected", "value"),  # User selection
637             Input("dd-ctrl-phy", "value"),
638             Input("dd-ctrl-area", "value"),
639             Input("dd-ctrl-test", "value"),
640             Input("cl-ctrl-core", "value"),
641             Input("cl-ctrl-core-all", "value"),
642             Input("cl-ctrl-framesize", "value"),
643             Input("cl-ctrl-framesize-all", "value"),
644             Input("cl-ctrl-testtype", "value"),
645             Input("cl-ctrl-testtype-all", "value"),
646             Input("btn-ctrl-add", "n_clicks"),
647             Input("dpr-period", "start_date"),
648             Input("dpr-period", "end_date"),
649             Input("btn-sel-remove", "n_clicks"),
650             Input("btn-sel-remove-all", "n_clicks"),
651         )
652         def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
653             dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
654             cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
655             cl_testtype: list, cl_testtype_all: list, btn_add: int,
656             d_start: str, d_end: str, btn_remove: int,
657             btn_remove_all: int) -> tuple:
658             """
659             """
660
661             d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
662                 int(d_start[8:10]))
663             d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
664
665             row_fig_tput = no_update
666             row_fig_lat = no_update
667             row_btn_dwnld = no_update
668             row_card_sel_tests = no_update
669             row_btns_sel_tests = no_update
670
671             ctrl_panel = self.ControlPanel(cp_data)
672
673             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
674
675             if trigger_id == "dd-ctrl-phy":
676                 try:
677                     options = [
678                         {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
679                             for v in [v for v in self.spec_tbs[dd_phy].keys()]
680                     ]
681                     disabled = False
682                 except KeyError:
683                     options = list()
684                     disabled = no_update
685                 ctrl_panel.set({
686                     "dd-ctrl-phy-value": dd_phy,
687                     "dd-ctrl-area-value": str(),
688                     "dd-ctrl-area-options": options,
689                     "dd-ctrl-area-disabled": disabled,
690                     "dd-ctrl-test-options": list(),
691                     "dd-ctrl-test-disabled": True,
692                     "cl-ctrl-core-options": list(),
693                     "cl-ctrl-core-value": list(),
694                     "cl-ctrl-core-all-value": list(),
695                     "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
696                     "cl-ctrl-framesize-options": list(),
697                     "cl-ctrl-framesize-value": list(),
698                     "cl-ctrl-framesize-all-value": list(),
699                     "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
700                     "cl-ctrl-testtype-options": list(),
701                     "cl-ctrl-testtype-value": list(),
702                     "cl-ctrl-testtype-all-value": list(),
703                     "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
704                     "btn-ctrl-add-disabled": True,
705                 })
706             elif trigger_id == "dd-ctrl-area":
707                 try:
708                     phy = ctrl_panel.get("dd-ctrl-phy-value")
709                     options = [
710                         {"label": v, "value": v}
711                             for v in self.spec_tbs[phy][dd_area]["test"]
712                     ]
713                     disabled = False
714                 except KeyError:
715                     options = list()
716                     disabled = True
717                 ctrl_panel.set({
718                     "dd-ctrl-area-value": dd_area,
719                     "dd-ctrl-test-value": str(),
720                     "dd-ctrl-test-options": options,
721                     "dd-ctrl-test-disabled": disabled,
722                     "cl-ctrl-core-options": list(),
723                     "cl-ctrl-core-value": list(),
724                     "cl-ctrl-core-all-value": list(),
725                     "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
726                     "cl-ctrl-framesize-options": list(),
727                     "cl-ctrl-framesize-value": list(),
728                     "cl-ctrl-framesize-all-value": list(),
729                     "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
730                     "cl-ctrl-testtype-options": list(),
731                     "cl-ctrl-testtype-value": list(),
732                     "cl-ctrl-testtype-all-value": list(),
733                     "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
734                     "btn-ctrl-add-disabled": True,
735                 })
736             elif trigger_id == "dd-ctrl-test":
737                 core_opts = list()
738                 framesize_opts = list()
739                 testtype_opts = list()
740                 phy = ctrl_panel.get("dd-ctrl-phy-value")
741                 area = ctrl_panel.get("dd-ctrl-area-value")
742                 if phy and area and dd_test:
743                     core_opts = [
744                         {"label": v, "value": v}
745                             for v in self.spec_tbs[phy][area]["core"]
746                     ]
747                     framesize_opts = [
748                         {"label": v, "value": v}
749                             for v in self.spec_tbs[phy][area]["frame-size"]
750                     ]
751                     testtype_opts = [
752                         {"label": v, "value": v}
753                             for v in self.spec_tbs[phy][area]["test-type"]
754                     ]
755                     ctrl_panel.set({
756                         "dd-ctrl-test-value": dd_test,
757                         "cl-ctrl-core-options": core_opts,
758                         "cl-ctrl-core-value": list(),
759                         "cl-ctrl-core-all-value": list(),
760                         "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
761                         "cl-ctrl-framesize-options": framesize_opts,
762                         "cl-ctrl-framesize-value": list(),
763                         "cl-ctrl-framesize-all-value": list(),
764                         "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
765                         "cl-ctrl-testtype-options": testtype_opts,
766                         "cl-ctrl-testtype-value": list(),
767                         "cl-ctrl-testtype-all-value": list(),
768                         "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
769                         "btn-ctrl-add-disabled": False,
770                     })
771             elif trigger_id == "cl-ctrl-core":
772                 val_sel, val_all = self._sync_checklists(
773                     opt=ctrl_panel.get("cl-ctrl-core-options"),
774                     sel=cl_core,
775                     all=list(),
776                     id=""
777                 )
778                 ctrl_panel.set({
779                     "cl-ctrl-core-value": val_sel,
780                     "cl-ctrl-core-all-value": val_all,
781                 })
782             elif trigger_id == "cl-ctrl-core-all":
783                 val_sel, val_all = self._sync_checklists(
784                     opt = ctrl_panel.get("cl-ctrl-core-options"),
785                     sel=list(),
786                     all=cl_core_all,
787                     id="all"
788                 )
789                 ctrl_panel.set({
790                     "cl-ctrl-core-value": val_sel,
791                     "cl-ctrl-core-all-value": val_all,
792                 })
793             elif trigger_id == "cl-ctrl-framesize":
794                 val_sel, val_all = self._sync_checklists(
795                     opt = ctrl_panel.get("cl-ctrl-framesize-options"),
796                     sel=cl_framesize,
797                     all=list(),
798                     id=""
799                 )
800                 ctrl_panel.set({
801                     "cl-ctrl-framesize-value": val_sel,
802                     "cl-ctrl-framesize-all-value": val_all,
803                 })
804             elif trigger_id == "cl-ctrl-framesize-all":
805                 val_sel, val_all = self._sync_checklists(
806                     opt = ctrl_panel.get("cl-ctrl-framesize-options"),
807                     sel=list(),
808                     all=cl_framesize_all,
809                     id="all"
810                 )
811                 ctrl_panel.set({
812                     "cl-ctrl-framesize-value": val_sel,
813                     "cl-ctrl-framesize-all-value": val_all,
814                 })
815             elif trigger_id == "cl-ctrl-testtype":
816                 val_sel, val_all = self._sync_checklists(
817                     opt = ctrl_panel.get("cl-ctrl-testtype-options"),
818                     sel=cl_testtype,
819                     all=list(),
820                     id=""
821                 )
822                 ctrl_panel.set({
823                     "cl-ctrl-testtype-value": val_sel,
824                     "cl-ctrl-testtype-all-value": val_all,
825                 })
826             elif trigger_id == "cl-ctrl-testtype-all":
827                 val_sel, val_all = self._sync_checklists(
828                     opt = ctrl_panel.get("cl-ctrl-testtype-options"),
829                     sel=list(),
830                     all=cl_testtype_all,
831                     id="all"
832                 )
833                 ctrl_panel.set({
834                     "cl-ctrl-testtype-value": val_sel,
835                     "cl-ctrl-testtype-all-value": val_all,
836                 })
837             elif trigger_id == "btn-ctrl-add":
838                 _ = btn_add
839                 phy = ctrl_panel.get("dd-ctrl-phy-value")
840                 area = ctrl_panel.get("dd-ctrl-area-value")
841                 test = ctrl_panel.get("dd-ctrl-test-value")
842                 cores = ctrl_panel.get("cl-ctrl-core-value")
843                 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
844                 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
845                 # Add selected test to the list of tests in store:
846                 if phy and area and test and cores and framesizes and testtypes:
847                     if store_sel is None:
848                         store_sel = list()
849                     for core in cores:
850                         for framesize in framesizes:
851                             for ttype in testtypes:
852                                 tid = (
853                                     f"{phy.replace('af_xdp', 'af-xdp')}-"
854                                     f"{area}-"
855                                     f"{framesize.lower()}-"
856                                     f"{core.lower()}-"
857                                     f"{test}-"
858                                     f"{ttype.lower()}"
859                                 )
860                                 if tid not in [itm["id"] for itm in store_sel]:
861                                     store_sel.append({
862                                         "id": tid,
863                                         "phy": phy,
864                                         "area": area,
865                                         "test": test,
866                                         "framesize": framesize.lower(),
867                                         "core": core.lower(),
868                                         "testtype": ttype.lower()
869                                     })
870                     row_card_sel_tests = self.STYLE_ENABLED
871                     row_btns_sel_tests = self.STYLE_ENABLED
872                     ctrl_panel.set(ctrl_panel.defaults)
873                     ctrl_panel.set({
874                         "cl-selected-options": self._list_tests(store_sel)
875                     })
876                     row_fig_tput, row_fig_lat, row_btn_dwnld = \
877                     _generate_plotting_arrea(
878                         graph_trending(
879                             self.data, store_sel, self.layout, d_start, d_end
880                         )
881                     )
882             elif trigger_id == "dpr-period":
883                 row_fig_tput, row_fig_lat, row_btn_dwnld = \
884                     _generate_plotting_arrea(
885                         graph_trending(
886                             self.data, store_sel, self.layout, d_start, d_end
887                         )
888                     )
889             elif trigger_id == "btn-sel-remove-all":
890                 _ = btn_remove_all
891                 row_fig_tput = self.PLACEHOLDER
892                 row_fig_lat = self.PLACEHOLDER
893                 row_btn_dwnld = self.PLACEHOLDER
894                 row_card_sel_tests = self.STYLE_DISABLED
895                 row_btns_sel_tests = self.STYLE_DISABLED
896                 store_sel = list()
897                 ctrl_panel.set({
898                         "cl-selected-options": list()
899                 })
900             elif trigger_id == "btn-sel-remove":
901                 _ = btn_remove
902                 if list_sel:
903                     new_store_sel = list()
904                     for item in store_sel:
905                         if item["id"] not in list_sel:
906                             new_store_sel.append(item)
907                     store_sel = new_store_sel
908                 if store_sel:
909                     row_fig_tput, row_fig_lat, row_btn_dwnld = \
910                     _generate_plotting_arrea(
911                         graph_trending(
912                             self.data, store_sel, self.layout, d_start, d_end
913                         )
914                     )
915                     ctrl_panel.set({
916                         "cl-selected-options": self._list_tests(store_sel)
917                     })
918                 else:
919                     row_fig_tput = self.PLACEHOLDER
920                     row_fig_lat = self.PLACEHOLDER
921                     row_btn_dwnld = self.PLACEHOLDER
922                     row_card_sel_tests = self.STYLE_DISABLED
923                     row_btns_sel_tests = self.STYLE_DISABLED
924                     store_sel = list()
925                     ctrl_panel.set({
926                             "cl-selected-options": list()
927                     })
928
929             ret_val = [
930                 ctrl_panel.panel, store_sel,
931                 row_fig_tput, row_fig_lat, row_btn_dwnld,
932                 row_card_sel_tests, row_btns_sel_tests
933             ]
934             ret_val.extend(ctrl_panel.values())
935             return ret_val
936
937         @app.callback(
938             Output("metadata-tput-lat", "children"),
939             Output("metadata-hdrh-graph", "children"),
940             Output("offcanvas-metadata", "is_open"),
941             Input("graph-tput", "clickData"),
942             Input("graph-latency", "clickData")
943         )
944         def _show_metadata_from_graphs(
945             tput_data: dict, lat_data: dict) -> tuple:
946             """
947             """
948             if not (tput_data or lat_data):
949                 raise PreventUpdate
950
951             metadata = no_update
952             graph = list()
953
954             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
955             if trigger_id == "graph-tput":
956                 title = "Throughput"
957                 array = tput_data["points"][0]["text"].split("<br>")
958                 children = [
959                     dbc.ListGroupItem(
960                         [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
961                     ) for x in array
962                 ]
963             elif trigger_id == "graph-latency":
964                 title = "Latency"
965                 array = lat_data["points"][0]["text"].split("<br>")
966                 children = [
967                     dbc.ListGroupItem(
968                         [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
969                     ) for x in array
970                 ]
971                 hdrh_data = lat_data["points"][0].get("customdata", None)
972                 if hdrh_data:
973                     graph = [dbc.Card(
974                         class_name="gy-2 p-0",
975                         children=[
976                             dbc.CardHeader(hdrh_data.pop("name")),
977                             dbc.CardBody(children=[
978                                 dcc.Graph(
979                                     id="hdrh-latency-graph",
980                                     figure=graph_hdrh_latency(
981                                         hdrh_data, self.layout
982                                     )
983                                 )
984                             ])
985                         ])
986                     ]
987             metadata = [
988                 dbc.Card(
989                     class_name="gy-2 p-0",
990                     children=[
991                         dbc.CardHeader(children=[
992                             dcc.Clipboard(
993                                 target_id="tput-lat-metadata",
994                                 title="Copy",
995                                 style={"display": "inline-block"}
996                             ),
997                             title
998                         ]),
999                         dbc.CardBody(
1000                             id="tput-lat-metadata",
1001                             class_name="p-0",
1002                             children=[
1003                                 dbc.ListGroup(children, flush=True)
1004                             ]
1005                         )
1006                     ]
1007                 )
1008             ]
1009
1010             return metadata, graph, True
1011
1012         @app.callback(
1013             Output("download-data", "data"),
1014             State("selected-tests", "data"),
1015             Input("btn-download-data", "n_clicks"),
1016             prevent_initial_call=True
1017         )
1018         def _download_data(store_sel, n_clicks):
1019             """
1020             """
1021
1022             if not n_clicks:
1023                 raise PreventUpdate
1024
1025             if not store_sel:
1026                 raise PreventUpdate
1027
1028             df = pd.DataFrame()
1029             for itm in store_sel:
1030                 sel_data = select_trending_data(self.data, itm)
1031                 if sel_data is None:
1032                     continue
1033                 df = pd.concat([df, sel_data], ignore_index=True)
1034
1035             return dcc.send_data_frame(df.to_csv, "trending_data.csv")