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