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, 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(days=5)
72
73         data_ndrpdr = Data(
74             data_spec_file=self._data_spec_file,
75             debug=True
76         ).read_trending_ndrpdr(days=14)
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",
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                             className="d-flex justify-content-center",
432                             min_date_allowed=\
433                                 datetime.utcnow()-timedelta(days=180),
434                             max_date_allowed=datetime.utcnow(),
435                             initial_visible_month=datetime.utcnow(),
436                             start_date=datetime.utcnow() - timedelta(days=180),
437                             end_date=datetime.utcnow(),
438                             display_format="D MMMM YY"
439                         )
440                     ]
441                 ),
442                 dbc.Row(
443                     id="row-card-sel-tests",
444                     class_name="gy-1",
445                     style=self.STYLE_DISABLED,
446                     children=[
447                         dbc.Label(
448                             "Selected tests",
449                             class_name="p-0"
450                         ),
451                         dbc.Checklist(
452                             class_name="overflow-auto",
453                             id="cl-selected",
454                             options=[],
455                             inline=False,
456                             style={"max-height": "12em"},
457                         )
458                     ],
459                 ),
460                 dbc.Row(
461                     id="row-btns-sel-tests",
462                     style=self.STYLE_DISABLED,
463                     children=[
464                         dbc.ButtonGroup(
465                             children=[
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                             ],
481                             size="md",
482                         )
483                     ]
484                 ),
485             ]
486         )
487
488     class ControlPanel:
489         def __init__(self, panel: dict) -> None:
490
491             CL_ALL_DISABLED = [{
492                 "label": "All",
493                 "value": "all",
494                 "disabled": True
495             }]
496
497             # Defines also the order of keys
498             self._defaults = {
499                 "dd-ctrl-phy-value": str(),
500                 "dd-ctrl-area-options": list(),
501                 "dd-ctrl-area-disabled": True,
502                 "dd-ctrl-area-value": str(),
503                 "dd-ctrl-test-options": list(),
504                 "dd-ctrl-test-disabled": True,
505                 "dd-ctrl-test-value": str(),
506                 "cl-ctrl-core-options": list(),
507                 "cl-ctrl-core-value": list(),
508                 "cl-ctrl-core-all-value": list(),
509                 "cl-ctrl-core-all-options": CL_ALL_DISABLED,
510                 "cl-ctrl-framesize-options": list(),
511                 "cl-ctrl-framesize-value": list(),
512                 "cl-ctrl-framesize-all-value": list(),
513                 "cl-ctrl-framesize-all-options": CL_ALL_DISABLED,
514                 "cl-ctrl-testtype-options": list(),
515                 "cl-ctrl-testtype-value": list(),
516                 "cl-ctrl-testtype-all-value": list(),
517                 "cl-ctrl-testtype-all-options": CL_ALL_DISABLED,
518                 "btn-ctrl-add-disabled": True,
519                 "cl-selected-options": list(),
520             }
521
522             self._panel = deepcopy(self._defaults)
523             if panel:
524                 for key in self._defaults:
525                     self._panel[key] = panel[key]
526
527         @property
528         def defaults(self) -> dict:
529             return self._defaults
530
531         @property
532         def panel(self) -> dict:
533             return self._panel
534
535         def set(self, kwargs: dict) -> None:
536             for key, val in kwargs.items():
537                 if key in self._panel:
538                     self._panel[key] = val
539                 else:
540                     raise KeyError(f"The key {key} is not defined.")
541
542         def get(self, key: str) -> any:
543             return self._panel[key]
544
545         def values(self) -> tuple:
546             return tuple(self._panel.values())
547
548     @staticmethod
549     def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
550         """
551         """
552         options = {v["value"] for v in opt}
553         if id =="all":
554             sel = list(options) if all else list()
555         else:
556             all = ["all", ] if set(sel) == options else list()
557         return sel, all
558
559     @staticmethod
560     def _list_tests(selection: dict) -> list:
561         """Display selected tests with checkboxes
562         """
563         if selection:
564             return [
565                 {"label": v["id"], "value": v["id"]} for v in selection
566             ]
567         else:
568             return list()
569
570     def callbacks(self, app):
571
572         def _generate_plotting_arrea(args: tuple) -> tuple:
573             """
574             """
575
576             (fig_tput, fig_lat) = args
577
578             row_fig_tput = self.PLACEHOLDER
579             row_fig_lat = self.PLACEHOLDER
580             row_btn_dwnld = self.PLACEHOLDER
581
582             if fig_tput:
583                 row_fig_tput = [
584                     dcc.Loading(
585                         dcc.Graph(
586                             id={"type": "graph", "index": "tput"},
587                             figure=fig_tput
588                         )
589                     ),
590                 ]
591                 row_btn_dwnld = [
592                     dcc.Loading(children=[
593                         dbc.Button(
594                             id="btn-download-data",
595                             children=["Download Data"]
596                         ),
597                         dcc.Download(id="download-data")
598                     ]),
599                 ]
600             if fig_lat:
601                 row_fig_lat = [
602                     dcc.Loading(
603                         dcc.Graph(
604                             id={"type": "graph", "index": "lat"},
605                             figure=fig_lat
606                         )
607                     )
608                 ]
609
610             return row_fig_tput, row_fig_lat, row_btn_dwnld
611
612         @app.callback(
613             Output("control-panel", "data"),  # Store
614             Output("selected-tests", "data"),  # Store
615             Output("row-graph-tput", "children"),
616             Output("row-graph-lat", "children"),
617             Output("row-btn-download", "children"),
618             Output("row-card-sel-tests", "style"),
619             Output("row-btns-sel-tests", "style"),
620             Output("dd-ctrl-phy", "value"),
621             Output("dd-ctrl-area", "options"),
622             Output("dd-ctrl-area", "disabled"),
623             Output("dd-ctrl-area", "value"),
624             Output("dd-ctrl-test", "options"),
625             Output("dd-ctrl-test", "disabled"),
626             Output("dd-ctrl-test", "value"),
627             Output("cl-ctrl-core", "options"),
628             Output("cl-ctrl-core", "value"),
629             Output("cl-ctrl-core-all", "value"),
630             Output("cl-ctrl-core-all", "options"),
631             Output("cl-ctrl-framesize", "options"),
632             Output("cl-ctrl-framesize", "value"),
633             Output("cl-ctrl-framesize-all", "value"),
634             Output("cl-ctrl-framesize-all", "options"),
635             Output("cl-ctrl-testtype", "options"),
636             Output("cl-ctrl-testtype", "value"),
637             Output("cl-ctrl-testtype-all", "value"),
638             Output("cl-ctrl-testtype-all", "options"),
639             Output("btn-ctrl-add", "disabled"),
640             Output("cl-selected", "options"),  # User selection
641             State("control-panel", "data"),  # Store
642             State("selected-tests", "data"),  # Store
643             State("cl-selected", "value"),  # User selection
644             Input("dd-ctrl-phy", "value"),
645             Input("dd-ctrl-area", "value"),
646             Input("dd-ctrl-test", "value"),
647             Input("cl-ctrl-core", "value"),
648             Input("cl-ctrl-core-all", "value"),
649             Input("cl-ctrl-framesize", "value"),
650             Input("cl-ctrl-framesize-all", "value"),
651             Input("cl-ctrl-testtype", "value"),
652             Input("cl-ctrl-testtype-all", "value"),
653             Input("btn-ctrl-add", "n_clicks"),
654             Input("dpr-period", "start_date"),
655             Input("dpr-period", "end_date"),
656             Input("btn-sel-remove", "n_clicks"),
657             Input("btn-sel-remove-all", "n_clicks"),
658         )
659         def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
660             dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
661             cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
662             cl_testtype: list, cl_testtype_all: list, btn_add: int,
663             d_start: str, d_end: str, btn_remove: int,
664             btn_remove_all: int) -> tuple:
665             """
666             """
667
668             d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
669                 int(d_start[8:10]))
670             d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
671
672             row_fig_tput = no_update
673             row_fig_lat = no_update
674             row_btn_dwnld = no_update
675             row_card_sel_tests = no_update
676             row_btns_sel_tests = no_update
677
678             ctrl_panel = self.ControlPanel(cp_data)
679
680             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
681
682             if trigger_id == "dd-ctrl-phy":
683                 try:
684                     options = [
685                         {"label": self.spec_tbs[dd_phy][v]["label"], "value": v}
686                             for v in [v for v in self.spec_tbs[dd_phy].keys()]
687                     ]
688                     disabled = False
689                 except KeyError:
690                     options = list()
691                     disabled = no_update
692                 ctrl_panel.set({
693                     "dd-ctrl-phy-value": dd_phy,
694                     "dd-ctrl-area-value": str(),
695                     "dd-ctrl-area-options": options,
696                     "dd-ctrl-area-disabled": disabled,
697                     "dd-ctrl-test-options": list(),
698                     "dd-ctrl-test-disabled": True,
699                     "cl-ctrl-core-options": list(),
700                     "cl-ctrl-core-value": list(),
701                     "cl-ctrl-core-all-value": list(),
702                     "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
703                     "cl-ctrl-framesize-options": list(),
704                     "cl-ctrl-framesize-value": list(),
705                     "cl-ctrl-framesize-all-value": list(),
706                     "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
707                     "cl-ctrl-testtype-options": list(),
708                     "cl-ctrl-testtype-value": list(),
709                     "cl-ctrl-testtype-all-value": list(),
710                     "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
711                     "btn-ctrl-add-disabled": True,
712                 })
713             elif trigger_id == "dd-ctrl-area":
714                 try:
715                     phy = ctrl_panel.get("dd-ctrl-phy-value")
716                     options = [
717                         {"label": v, "value": v}
718                             for v in self.spec_tbs[phy][dd_area]["test"]
719                     ]
720                     disabled = False
721                 except KeyError:
722                     options = list()
723                     disabled = True
724                 ctrl_panel.set({
725                     "dd-ctrl-area-value": dd_area,
726                     "dd-ctrl-test-value": str(),
727                     "dd-ctrl-test-options": options,
728                     "dd-ctrl-test-disabled": disabled,
729                     "cl-ctrl-core-options": list(),
730                     "cl-ctrl-core-value": list(),
731                     "cl-ctrl-core-all-value": list(),
732                     "cl-ctrl-core-all-options": self.CL_ALL_DISABLED,
733                     "cl-ctrl-framesize-options": list(),
734                     "cl-ctrl-framesize-value": list(),
735                     "cl-ctrl-framesize-all-value": list(),
736                     "cl-ctrl-framesize-all-options": self.CL_ALL_DISABLED,
737                     "cl-ctrl-testtype-options": list(),
738                     "cl-ctrl-testtype-value": list(),
739                     "cl-ctrl-testtype-all-value": list(),
740                     "cl-ctrl-testtype-all-options": self.CL_ALL_DISABLED,
741                     "btn-ctrl-add-disabled": True,
742                 })
743             elif trigger_id == "dd-ctrl-test":
744                 core_opts = list()
745                 framesize_opts = list()
746                 testtype_opts = list()
747                 phy = ctrl_panel.get("dd-ctrl-phy-value")
748                 area = ctrl_panel.get("dd-ctrl-area-value")
749                 if phy and area and dd_test:
750                     core_opts = [
751                         {"label": v, "value": v}
752                             for v in self.spec_tbs[phy][area]["core"]
753                     ]
754                     framesize_opts = [
755                         {"label": v, "value": v}
756                             for v in self.spec_tbs[phy][area]["frame-size"]
757                     ]
758                     testtype_opts = [
759                         {"label": v, "value": v}
760                             for v in self.spec_tbs[phy][area]["test-type"]
761                     ]
762                     ctrl_panel.set({
763                         "dd-ctrl-test-value": dd_test,
764                         "cl-ctrl-core-options": core_opts,
765                         "cl-ctrl-core-value": list(),
766                         "cl-ctrl-core-all-value": list(),
767                         "cl-ctrl-core-all-options": self.CL_ALL_ENABLED,
768                         "cl-ctrl-framesize-options": framesize_opts,
769                         "cl-ctrl-framesize-value": list(),
770                         "cl-ctrl-framesize-all-value": list(),
771                         "cl-ctrl-framesize-all-options": self.CL_ALL_ENABLED,
772                         "cl-ctrl-testtype-options": testtype_opts,
773                         "cl-ctrl-testtype-value": list(),
774                         "cl-ctrl-testtype-all-value": list(),
775                         "cl-ctrl-testtype-all-options": self.CL_ALL_ENABLED,
776                         "btn-ctrl-add-disabled": False,
777                     })
778             elif trigger_id == "cl-ctrl-core":
779                 val_sel, val_all = self._sync_checklists(
780                     opt=ctrl_panel.get("cl-ctrl-core-options"),
781                     sel=cl_core,
782                     all=list(),
783                     id=""
784                 )
785                 ctrl_panel.set({
786                     "cl-ctrl-core-value": val_sel,
787                     "cl-ctrl-core-all-value": val_all,
788                 })
789             elif trigger_id == "cl-ctrl-core-all":
790                 val_sel, val_all = self._sync_checklists(
791                     opt = ctrl_panel.get("cl-ctrl-core-options"),
792                     sel=list(),
793                     all=cl_core_all,
794                     id="all"
795                 )
796                 ctrl_panel.set({
797                     "cl-ctrl-core-value": val_sel,
798                     "cl-ctrl-core-all-value": val_all,
799                 })
800             elif trigger_id == "cl-ctrl-framesize":
801                 val_sel, val_all = self._sync_checklists(
802                     opt = ctrl_panel.get("cl-ctrl-framesize-options"),
803                     sel=cl_framesize,
804                     all=list(),
805                     id=""
806                 )
807                 ctrl_panel.set({
808                     "cl-ctrl-framesize-value": val_sel,
809                     "cl-ctrl-framesize-all-value": val_all,
810                 })
811             elif trigger_id == "cl-ctrl-framesize-all":
812                 val_sel, val_all = self._sync_checklists(
813                     opt = ctrl_panel.get("cl-ctrl-framesize-options"),
814                     sel=list(),
815                     all=cl_framesize_all,
816                     id="all"
817                 )
818                 ctrl_panel.set({
819                     "cl-ctrl-framesize-value": val_sel,
820                     "cl-ctrl-framesize-all-value": val_all,
821                 })
822             elif trigger_id == "cl-ctrl-testtype":
823                 val_sel, val_all = self._sync_checklists(
824                     opt = ctrl_panel.get("cl-ctrl-testtype-options"),
825                     sel=cl_testtype,
826                     all=list(),
827                     id=""
828                 )
829                 ctrl_panel.set({
830                     "cl-ctrl-testtype-value": val_sel,
831                     "cl-ctrl-testtype-all-value": val_all,
832                 })
833             elif trigger_id == "cl-ctrl-testtype-all":
834                 val_sel, val_all = self._sync_checklists(
835                     opt = ctrl_panel.get("cl-ctrl-testtype-options"),
836                     sel=list(),
837                     all=cl_testtype_all,
838                     id="all"
839                 )
840                 ctrl_panel.set({
841                     "cl-ctrl-testtype-value": val_sel,
842                     "cl-ctrl-testtype-all-value": val_all,
843                 })
844             elif trigger_id == "btn-ctrl-add":
845                 _ = btn_add
846                 phy = ctrl_panel.get("dd-ctrl-phy-value")
847                 area = ctrl_panel.get("dd-ctrl-area-value")
848                 test = ctrl_panel.get("dd-ctrl-test-value")
849                 cores = ctrl_panel.get("cl-ctrl-core-value")
850                 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
851                 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
852                 # Add selected test to the list of tests in store:
853                 if phy and area and test and cores and framesizes and testtypes:
854                     if store_sel is None:
855                         store_sel = list()
856                     for core in cores:
857                         for framesize in framesizes:
858                             for ttype in testtypes:
859                                 tid = (
860                                     f"{phy.replace('af_xdp', 'af-xdp')}-"
861                                     f"{area}-"
862                                     f"{framesize.lower()}-"
863                                     f"{core.lower()}-"
864                                     f"{test}-"
865                                     f"{ttype.lower()}"
866                                 )
867                                 if tid not in [itm["id"] for itm in store_sel]:
868                                     store_sel.append({
869                                         "id": tid,
870                                         "phy": phy,
871                                         "area": area,
872                                         "test": test,
873                                         "framesize": framesize.lower(),
874                                         "core": core.lower(),
875                                         "testtype": ttype.lower()
876                                     })
877                     row_card_sel_tests = self.STYLE_ENABLED
878                     row_btns_sel_tests = self.STYLE_ENABLED
879                     ctrl_panel.set(ctrl_panel.defaults)
880                     ctrl_panel.set({
881                         "cl-selected-options": self._list_tests(store_sel)
882                     })
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 == "dpr-period":
890                 row_fig_tput, row_fig_lat, row_btn_dwnld = \
891                     _generate_plotting_arrea(
892                         graph_trending(
893                             self.data, store_sel, self.layout, d_start, d_end
894                         )
895                     )
896             elif trigger_id == "btn-sel-remove-all":
897                 _ = btn_remove_all
898                 row_fig_tput = self.PLACEHOLDER
899                 row_fig_lat = self.PLACEHOLDER
900                 row_btn_dwnld = self.PLACEHOLDER
901                 row_card_sel_tests = self.STYLE_DISABLED
902                 row_btns_sel_tests = self.STYLE_DISABLED
903                 store_sel = list()
904                 ctrl_panel.set({
905                         "cl-selected-options": list()
906                 })
907             elif trigger_id == "btn-sel-remove":
908                 _ = btn_remove
909                 if list_sel:
910                     new_store_sel = list()
911                     for item in store_sel:
912                         if item["id"] not in list_sel:
913                             new_store_sel.append(item)
914                     store_sel = new_store_sel
915                 if store_sel:
916                     row_fig_tput, row_fig_lat, row_btn_dwnld = \
917                     _generate_plotting_arrea(
918                         graph_trending(
919                             self.data, store_sel, self.layout, d_start, d_end
920                         )
921                     )
922                     ctrl_panel.set({
923                         "cl-selected-options": self._list_tests(store_sel)
924                     })
925                 else:
926                     row_fig_tput = self.PLACEHOLDER
927                     row_fig_lat = self.PLACEHOLDER
928                     row_btn_dwnld = self.PLACEHOLDER
929                     row_card_sel_tests = self.STYLE_DISABLED
930                     row_btns_sel_tests = self.STYLE_DISABLED
931                     store_sel = list()
932                     ctrl_panel.set({
933                             "cl-selected-options": list()
934                     })
935
936             ret_val = [
937                 ctrl_panel.panel, store_sel,
938                 row_fig_tput, row_fig_lat, row_btn_dwnld,
939                 row_card_sel_tests, row_btns_sel_tests
940             ]
941             ret_val.extend(ctrl_panel.values())
942             return ret_val
943
944         @app.callback(
945             Output("metadata-tput-lat", "children"),
946             Output("metadata-hdrh-graph", "children"),
947             Output("offcanvas-metadata", "is_open"),
948             Input({"type": "graph", "index": ALL}, "clickData"),
949             prevent_initial_call=True
950         )
951         def _show_metadata_from_graphs(graph_data: dict) -> tuple:
952             """
953             """
954             try:
955                 trigger_id = loads(
956                     callback_context.triggered[0]["prop_id"].split(".")[0]
957                 )["index"]
958                 idx = 0 if trigger_id == "tput" else 1
959                 graph_data = graph_data[idx]["points"][0]
960             except (JSONDecodeError, IndexError, KeyError, ValueError,
961                     TypeError):
962                 raise PreventUpdate
963
964             metadata = no_update
965             graph = list()
966
967             children = [
968                 dbc.ListGroupItem(
969                     [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
970                 ) for x in graph_data.get("text", "").split("<br>")
971             ]
972             if trigger_id == "tput":
973                 title = "Throughput"
974             elif trigger_id == "lat":
975                 title = "Latency"
976                 hdrh_data = graph_data.get("customdata", None)
977                 if hdrh_data:
978                     graph = [dbc.Card(
979                         class_name="gy-2 p-0",
980                         children=[
981                             dbc.CardHeader(hdrh_data.pop("name")),
982                             dbc.CardBody(children=[
983                                 dcc.Graph(
984                                     id="hdrh-latency-graph",
985                                     figure=graph_hdrh_latency(
986                                         hdrh_data, self.layout
987                                     )
988                                 )
989                             ])
990                         ])
991                     ]
992             metadata = [
993                 dbc.Card(
994                     class_name="gy-2 p-0",
995                     children=[
996                         dbc.CardHeader(children=[
997                             dcc.Clipboard(
998                                 target_id="tput-lat-metadata",
999                                 title="Copy",
1000                                 style={"display": "inline-block"}
1001                             ),
1002                             title
1003                         ]),
1004                         dbc.CardBody(
1005                             id="tput-lat-metadata",
1006                             class_name="p-0",
1007                             children=[dbc.ListGroup(children, flush=True), ]
1008                         )
1009                     ]
1010                 )
1011             ]
1012
1013             return metadata, graph, True
1014
1015         @app.callback(
1016             Output("download-data", "data"),
1017             State("selected-tests", "data"),
1018             Input("btn-download-data", "n_clicks"),
1019             prevent_initial_call=True
1020         )
1021         def _download_data(store_sel, n_clicks):
1022             """
1023             """
1024
1025             if not n_clicks:
1026                 raise PreventUpdate
1027
1028             if not store_sel:
1029                 raise PreventUpdate
1030
1031             df = pd.DataFrame()
1032             for itm in store_sel:
1033                 sel_data = select_trending_data(self.data, itm)
1034                 if sel_data is None:
1035                     continue
1036                 df = pd.concat([df, sel_data], ignore_index=True)
1037
1038             return dcc.send_data_frame(df.to_csv, "trending_data.csv")