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