UTI: Code clean-up
[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 logging
18 import pandas as pd
19 import dash_bootstrap_components as dbc
20
21 from flask import Flask
22 from dash import dcc
23 from dash import html
24 from dash import callback_context, no_update, ALL
25 from dash import Input, Output, State
26 from dash.exceptions import PreventUpdate
27 from yaml import load, FullLoader, YAMLError
28 from datetime import datetime, timedelta
29 from copy import deepcopy
30 from json import loads, JSONDecodeError
31 from ast import literal_eval
32
33 from ..utils.constants import Constants as C
34 from ..utils.utils import show_tooltip, label, sync_checklists, list_tests, \
35     get_date, gen_new_url
36 from ..utils.url_processing import url_decode
37 from ..data.data import Data
38 from .graphs import graph_trending, graph_hdrh_latency, \
39     select_trending_data
40
41
42 class Layout:
43     """The layout of the dash app and the callbacks.
44     """
45
46     def __init__(self, app: Flask, html_layout_file: str,
47         graph_layout_file: str, data_spec_file: str, tooltip_file: str,
48         time_period: str=None) -> None:
49         """Initialization:
50         - save the input parameters,
51         - read and pre-process the data,
52         - prepare data for the control panel,
53         - read HTML layout file,
54         - read tooltips from the tooltip file.
55
56         :param app: Flask application running the dash application.
57         :param html_layout_file: Path and name of the file specifying the HTML
58             layout of the dash application.
59         :param graph_layout_file: Path and name of the file with layout of
60             plot.ly graphs.
61         :param data_spec_file: Path and name of the file specifying the data to
62             be read from parquets for this application.
63         :param tooltip_file: Path and name of the yaml file specifying the
64             tooltips.
65         :param time_period: It defines the time period for data read from the
66             parquets in days from now back to the past.
67         :type app: Flask
68         :type html_layout_file: str
69         :type graph_layout_file: str
70         :type data_spec_file: str
71         :type tooltip_file: str
72         :type time_period: int
73         """
74
75         # Inputs
76         self._app = app
77         self._html_layout_file = html_layout_file
78         self._graph_layout_file = graph_layout_file
79         self._data_spec_file = data_spec_file
80         self._tooltip_file = tooltip_file
81         self._time_period = time_period
82
83         # Read the data:
84         data_mrr = Data(
85             data_spec_file=self._data_spec_file,
86             debug=True
87         ).read_trending_mrr(days=self._time_period)
88
89         data_ndrpdr = Data(
90             data_spec_file=self._data_spec_file,
91             debug=True
92         ).read_trending_ndrpdr(days=self._time_period)
93
94         self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
95
96         data_time_period = \
97             (datetime.utcnow() - self._data["start_time"].min()).days
98         if self._time_period > data_time_period:
99             self._time_period = data_time_period
100
101
102         # Get structure of tests:
103         tbs = dict()
104         for _, row in self._data[["job", "test_id"]].drop_duplicates().\
105                 iterrows():
106             lst_job = row["job"].split("-")
107             dut = lst_job[1]
108             ttype = lst_job[3]
109             tbed = "-".join(lst_job[-2:])
110             lst_test = row["test_id"].split(".")
111             if dut == "dpdk":
112                 area = "dpdk"
113             else:
114                 area = "-".join(lst_test[3:-2])
115             suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\
116                 replace("2n-", "")
117             test = lst_test[-1]
118             nic = suite.split("-")[0]
119             for drv in C.DRIVERS:
120                 if drv in test:
121                     if drv == "af-xdp":
122                         driver = "af_xdp"
123                     else:
124                         driver = drv
125                     test = test.replace(f"{drv}-", "")
126                     break
127             else:
128                 driver = "dpdk"
129             infra = "-".join((tbed, nic, driver))
130             lst_test = test.split("-")
131             framesize = lst_test[0]
132             core = lst_test[1] if lst_test[1] else "8C"
133             test = "-".join(lst_test[2: -1])
134
135             if tbs.get(dut, None) is None:
136                 tbs[dut] = dict()
137             if tbs[dut].get(infra, None) is None:
138                 tbs[dut][infra] = dict()
139             if tbs[dut][infra].get(area, None) is None:
140                 tbs[dut][infra][area] = dict()
141             if tbs[dut][infra][area].get(test, None) is None:
142                 tbs[dut][infra][area][test] = dict()
143                 tbs[dut][infra][area][test]["core"] = list()
144                 tbs[dut][infra][area][test]["frame-size"] = list()
145                 tbs[dut][infra][area][test]["test-type"] = list()
146             if core.upper() not in tbs[dut][infra][area][test]["core"]:
147                 tbs[dut][infra][area][test]["core"].append(core.upper())
148             if framesize.upper() not in \
149                     tbs[dut][infra][area][test]["frame-size"]:
150                 tbs[dut][infra][area][test]["frame-size"].append(
151                     framesize.upper())
152             if ttype == "mrr":
153                 if "MRR" not in tbs[dut][infra][area][test]["test-type"]:
154                     tbs[dut][infra][area][test]["test-type"].append("MRR")
155             elif ttype == "ndrpdr":
156                 if "NDR" not in tbs[dut][infra][area][test]["test-type"]:
157                     tbs[dut][infra][area][test]["test-type"].extend(
158                         ("NDR", "PDR"))
159         self._spec_tbs = tbs
160
161         # Read from files:
162         self._html_layout = ""
163         self._graph_layout = None
164         self._tooltips = dict()
165
166         try:
167             with open(self._html_layout_file, "r") as file_read:
168                 self._html_layout = file_read.read()
169         except IOError as err:
170             raise RuntimeError(
171                 f"Not possible to open the file {self._html_layout_file}\n{err}"
172             )
173
174         try:
175             with open(self._graph_layout_file, "r") as file_read:
176                 self._graph_layout = load(file_read, Loader=FullLoader)
177         except IOError as err:
178             raise RuntimeError(
179                 f"Not possible to open the file {self._graph_layout_file}\n"
180                 f"{err}"
181             )
182         except YAMLError as err:
183             raise RuntimeError(
184                 f"An error occurred while parsing the specification file "
185                 f"{self._graph_layout_file}\n{err}"
186             )
187
188         try:
189             with open(self._tooltip_file, "r") as file_read:
190                 self._tooltips = load(file_read, Loader=FullLoader)
191         except IOError as err:
192             logging.warning(
193                 f"Not possible to open the file {self._tooltip_file}\n{err}"
194             )
195         except YAMLError as err:
196             logging.warning(
197                 f"An error occurred while parsing the specification file "
198                 f"{self._tooltip_file}\n{err}"
199             )
200
201         # Callbacks:
202         if self._app is not None and hasattr(self, 'callbacks'):
203             self.callbacks(self._app)
204
205     @property
206     def html_layout(self):
207         return self._html_layout
208
209     @property
210     def spec_tbs(self):
211         return self._spec_tbs
212
213     @property
214     def data(self):
215         return self._data
216
217     @property
218     def layout(self):
219         return self._graph_layout
220
221     @property
222     def time_period(self):
223         return self._time_period
224
225     def add_content(self):
226         """Top level method which generated the web page.
227
228         It generates:
229         - Store for user input data,
230         - Navigation bar,
231         - Main area with control panel and ploting area.
232
233         If no HTML layout is provided, an error message is displayed instead.
234
235         :returns: The HTML div with the whole page.
236         :rtype: html.Div
237         """
238
239         if self.html_layout and self.spec_tbs:
240             return html.Div(
241                 id="div-main",
242                 children=[
243                     dbc.Row(
244                         id="row-navbar",
245                         class_name="g-0",
246                         children=[
247                             self._add_navbar(),
248                         ]
249                     ),
250                     dcc.Loading(
251                         dbc.Offcanvas(
252                             class_name="w-50",
253                             id="offcanvas-metadata",
254                             title="Throughput And Latency",
255                             placement="end",
256                             is_open=False,
257                             children=[
258                                 dbc.Row(id="metadata-tput-lat"),
259                                 dbc.Row(id="metadata-hdrh-graph"),
260                             ]
261                         )
262                     ),
263                     dbc.Row(
264                         id="row-main",
265                         class_name="g-0",
266                         children=[
267                             dcc.Store(id="selected-tests"),
268                             dcc.Store(id="control-panel"),
269                             dcc.Location(id="url", refresh=False),
270                             self._add_ctrl_col(),
271                             self._add_plotting_col(),
272                         ]
273                     )
274                 ]
275             )
276         else:
277             return html.Div(
278                 id="div-main-error",
279                 children=[
280                     dbc.Alert(
281                         [
282                             "An Error Occured",
283                         ],
284                         color="danger",
285                     ),
286                 ]
287             )
288
289     def _add_navbar(self):
290         """Add nav element with navigation panel. It is placed on the top.
291
292         :returns: Navigation bar.
293         :rtype: dbc.NavbarSimple
294         """
295         return dbc.NavbarSimple(
296             id="navbarsimple-main",
297             children=[
298                 dbc.NavItem(
299                     dbc.NavLink(
300                         "Continuous Performance Trending",
301                         disabled=True,
302                         external_link=True,
303                         href="#"
304                     )
305                 )
306             ],
307             brand="Dashboard",
308             brand_href="/",
309             brand_external_link=True,
310             class_name="p-2",
311             fluid=True,
312         )
313
314     def _add_ctrl_col(self) -> dbc.Col:
315         """Add column with controls. It is placed on the left side.
316
317         :returns: Column with the control panel.
318         :rtype: dbc.Col
319         """
320         return dbc.Col(
321             id="col-controls",
322             children=[
323                 self._add_ctrl_panel(),
324             ],
325         )
326
327     def _add_plotting_col(self) -> dbc.Col:
328         """Add column with plots and tables. It is placed on the right side.
329
330         :returns: Column with tables.
331         :rtype: dbc.Col
332         """
333         return dbc.Col(
334             id="col-plotting-area",
335             children=[
336                 dcc.Loading(
337                     children=[
338                         dbc.Row(  # Throughput
339                             id="row-graph-tput",
340                             class_name="g-0 p-2",
341                             children=[
342                                 C.PLACEHOLDER
343                             ]
344                         ),
345                         dbc.Row(  # Latency
346                             id="row-graph-lat",
347                             class_name="g-0 p-2",
348                             children=[
349                                 C.PLACEHOLDER
350                             ]
351                         ),
352                         dbc.Row(  # Download
353                             id="row-btn-download",
354                             class_name="g-0 p-2",
355                             children=[
356                                 C.PLACEHOLDER
357                             ]
358                         )
359                     ]
360                 )
361             ],
362             width=9,
363         )
364
365     def _add_ctrl_panel(self) -> dbc.Row:
366         """Add control panel.
367
368         :returns: Control panel.
369         :rtype: dbc.Row
370         """
371         return dbc.Row(
372             id="row-ctrl-panel",
373             class_name="g-0 p-2",
374             children=[
375                 dbc.Row(
376                     class_name="g-0",
377                     children=[
378                         dbc.InputGroup(
379                             [
380                                 dbc.InputGroupText(
381                                     children=show_tooltip(self._tooltips,
382                                         "help-dut", "DUT")
383                                 ),
384                                 dbc.Select(
385                                     id="dd-ctrl-dut",
386                                     placeholder=(
387                                         "Select a Device under Test..."
388                                     ),
389                                     options=sorted(
390                                         [
391                                             {"label": k, "value": k} \
392                                                 for k in self.spec_tbs.keys()
393                                         ],
394                                         key=lambda d: d["label"]
395                                     )
396                                 )
397                             ],
398                             class_name="mb-3",
399                             size="sm",
400                         ),
401                     ]
402                 ),
403                 dbc.Row(
404                     class_name="g-0",
405                     children=[
406                         dbc.InputGroup(
407                             [
408                                 dbc.InputGroupText(
409                                     children=show_tooltip(self._tooltips,
410                                         "help-infra", "Infra")
411                                 ),
412                                 dbc.Select(
413                                     id="dd-ctrl-phy",
414                                     placeholder=(
415                                         "Select a Physical Test Bed "
416                                         "Topology..."
417                                     )
418                                 )
419                             ],
420                             class_name="mb-3",
421                             size="sm",
422                         ),
423                     ]
424                 ),
425                 dbc.Row(
426                     class_name="g-0",
427                     children=[
428                         dbc.InputGroup(
429                             [
430                                 dbc.InputGroupText(
431                                     children=show_tooltip(self._tooltips,
432                                         "help-area", "Area")
433                                 ),
434                                 dbc.Select(
435                                     id="dd-ctrl-area",
436                                     placeholder="Select an Area...",
437                                     disabled=True,
438                                 ),
439                             ],
440                             class_name="mb-3",
441                             size="sm",
442                         ),
443                     ]
444                 ),
445                 dbc.Row(
446                     class_name="g-0",
447                     children=[
448                         dbc.InputGroup(
449                             [
450                                 dbc.InputGroupText(
451                                     children=show_tooltip(self._tooltips,
452                                         "help-test", "Test")
453                                 ),
454                                 dbc.Select(
455                                     id="dd-ctrl-test",
456                                     placeholder="Select a Test...",
457                                     disabled=True,
458                                 ),
459                             ],
460                             class_name="mb-3",
461                             size="sm",
462                         ),
463                     ]
464                 ),
465                 dbc.Row(
466                     id="row-ctrl-framesize",
467                     class_name="gy-1",
468                     children=[
469                         dbc.Label(
470                             children=show_tooltip(self._tooltips,
471                                 "help-framesize", "Frame Size"),
472                             class_name="p-0"
473                         ),
474                         dbc.Col(
475                             children=[
476                                 dbc.Checklist(
477                                     id="cl-ctrl-framesize-all",
478                                     options=C.CL_ALL_DISABLED,
479                                     inline=True,
480                                     switch=False
481                                 ),
482                             ],
483                             width=3
484                         ),
485                         dbc.Col(
486                             children=[
487                                 dbc.Checklist(
488                                     id="cl-ctrl-framesize",
489                                     inline=True,
490                                     switch=False
491                                 )
492                             ]
493                         )
494                     ]
495                 ),
496                 dbc.Row(
497                     id="row-ctrl-core",
498                     class_name="gy-1",
499                     children=[
500                         dbc.Label(
501                             children=show_tooltip(self._tooltips,
502                                 "help-cores", "Number of Cores"),
503                             class_name="p-0"
504                         ),
505                         dbc.Col(
506                             children=[
507                                 dbc.Checklist(
508                                     id="cl-ctrl-core-all",
509                                     options=C.CL_ALL_DISABLED,
510                                     inline=False,
511                                     switch=False
512                                 )
513                             ],
514                             width=3
515                         ),
516                         dbc.Col(
517                             children=[
518                                 dbc.Checklist(
519                                     id="cl-ctrl-core",
520                                     inline=True,
521                                     switch=False
522                                 )
523                             ]
524                         )
525                     ]
526                 ),
527                 dbc.Row(
528                     id="row-ctrl-testtype",
529                     class_name="gy-1",
530                     children=[
531                         dbc.Label(
532                             children=show_tooltip(self._tooltips,
533                                 "help-ttype", "Test Type"),
534                             class_name="p-0"
535                         ),
536                         dbc.Col(
537                             children=[
538                                 dbc.Checklist(
539                                     id="cl-ctrl-testtype-all",
540                                     options=C.CL_ALL_DISABLED,
541                                     inline=True,
542                                     switch=False
543                                 ),
544                             ],
545                             width=3
546                         ),
547                         dbc.Col(
548                             children=[
549                                 dbc.Checklist(
550                                     id="cl-ctrl-testtype",
551                                     inline=True,
552                                     switch=False
553                                 )
554                             ]
555                         )
556                     ]
557                 ),
558                 dbc.Row(
559                     id="row-ctrl-normalize",
560                     class_name="gy-1",
561                     children=[
562                         dbc.Label(
563                             children=show_tooltip(self._tooltips,
564                                 "help-normalize", "Normalize"),
565                             class_name="p-0"
566                         ),
567                         dbc.Col(
568                             children=[
569                                 dbc.Checklist(
570                                     id="cl-ctrl-normalize",
571                                     options=[{
572                                         "value": "normalize",
573                                         "label": (
574                                             "Normalize results to CPU"
575                                             "frequency 2GHz"
576                                         )
577                                     }],
578                                     value=[],
579                                     inline=True,
580                                     switch=False
581                                 ),
582                             ]
583                         )
584                     ]
585                 ),
586                 dbc.Row(
587                     class_name="gy-1 p-0",
588                     children=[
589                         dbc.ButtonGroup(
590                             [
591                                 dbc.Button(
592                                     id="btn-ctrl-add",
593                                     children="Add Selected",
594                                     class_name="me-1",
595                                     color="info"
596                                 )
597                             ],
598                             size="md",
599                         )
600                     ]
601                 ),
602                 dbc.Row(
603                     class_name="gy-1",
604                     children=[
605                         dbc.Label(
606                             class_name="gy-1",
607                             children=show_tooltip(self._tooltips,
608                                 "help-time-period", "Time Period"),
609                         ),
610                         dcc.DatePickerRange(
611                             id="dpr-period",
612                             className="d-flex justify-content-center",
613                             min_date_allowed=\
614                                 datetime.utcnow() - timedelta(
615                                     days=self.time_period),
616                             max_date_allowed=datetime.utcnow(),
617                             initial_visible_month=datetime.utcnow(),
618                             start_date=\
619                                 datetime.utcnow() - timedelta(
620                                     days=self.time_period),
621                             end_date=datetime.utcnow(),
622                             display_format="D MMM YY"
623                         )
624                     ]
625                 ),
626                 dbc.Row(
627                     id="row-card-sel-tests",
628                     class_name="gy-1",
629                     style=C.STYLE_DISABLED,
630                     children=[
631                         dbc.Label(
632                             "Selected tests",
633                             class_name="p-0"
634                         ),
635                         dbc.Checklist(
636                             class_name="overflow-auto",
637                             id="cl-selected",
638                             options=[],
639                             inline=False,
640                             style={"max-height": "12em"},
641                         )
642                     ],
643                 ),
644                 dbc.Row(
645                     id="row-btns-sel-tests",
646                     style=C.STYLE_DISABLED,
647                     children=[
648                         dbc.ButtonGroup(
649                             class_name="gy-2",
650                             children=[
651                                 dbc.Button(
652                                     id="btn-sel-remove",
653                                     children="Remove Selected",
654                                     class_name="w-100 me-1",
655                                     color="info",
656                                     disabled=False
657                                 ),
658                                 dbc.Button(
659                                     id="btn-sel-remove-all",
660                                     children="Remove All",
661                                     class_name="w-100 me-1",
662                                     color="info",
663                                     disabled=False
664                                 ),
665                             ],
666                             size="md",
667                         )
668                     ]
669                 ),
670             ]
671         )
672
673     class ControlPanel:
674         """A class representing the control panel.
675         """
676
677         def __init__(self, panel: dict) -> None:
678             """Initialisation of the control pannel by default values. If
679             particular values are provided (parameter "panel") they are set
680             afterwards.
681
682             :param panel: Custom values to be set to the control panel.
683             :param default: Default values to be set to the control panel.
684             :type panel: dict
685             :type defaults: dict
686             """
687
688             # Defines also the order of keys
689             self._defaults = {
690                 "dd-ctrl-dut-value": str(),
691                 "dd-ctrl-phy-options": list(),
692                 "dd-ctrl-phy-disabled": True,
693                 "dd-ctrl-phy-value": str(),
694                 "dd-ctrl-area-options": list(),
695                 "dd-ctrl-area-disabled": True,
696                 "dd-ctrl-area-value": str(),
697                 "dd-ctrl-test-options": list(),
698                 "dd-ctrl-test-disabled": True,
699                 "dd-ctrl-test-value": str(),
700                 "cl-ctrl-core-options": list(),
701                 "cl-ctrl-core-value": list(),
702                 "cl-ctrl-core-all-value": list(),
703                 "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
704                 "cl-ctrl-framesize-options": list(),
705                 "cl-ctrl-framesize-value": list(),
706                 "cl-ctrl-framesize-all-value": list(),
707                 "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
708                 "cl-ctrl-testtype-options": list(),
709                 "cl-ctrl-testtype-value": list(),
710                 "cl-ctrl-testtype-all-value": list(),
711                 "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
712                 "btn-ctrl-add-disabled": True,
713                 "cl-normalize-value": list(),
714                 "cl-selected-options": list(),
715             }
716
717             self._panel = deepcopy(self._defaults)
718             if panel:
719                 for key in self._defaults:
720                     self._panel[key] = panel[key]
721
722         @property
723         def defaults(self) -> dict:
724             return self._defaults
725
726         @property
727         def panel(self) -> dict:
728             return self._panel
729
730         def set(self, kwargs: dict) -> None:
731             """Set the values of the Control panel.
732
733             :param kwargs: key - value pairs to be set.
734             :type kwargs: dict
735             :raises KeyError: If the key in kwargs is not present in the Control
736                 panel.
737             """
738             for key, val in kwargs.items():
739                 if key in self._panel:
740                     self._panel[key] = val
741                 else:
742                     raise KeyError(f"The key {key} is not defined.")
743
744         def get(self, key: str) -> any:
745             """Returns the value of a key from the Control panel.
746
747             :param key: The key which value should be returned.
748             :type key: str
749             :returns: The value of the key.
750             :rtype: any
751             :raises KeyError: If the key in kwargs is not present in the Control
752                 panel.
753             """
754             return self._panel[key]
755
756         def values(self) -> tuple:
757             """Returns the values from the Control panel as a list.
758
759             :returns: The values from the Control panel.
760             :rtype: list
761             """
762             return tuple(self._panel.values())
763
764     def callbacks(self, app):
765         """Callbacks for the whole application.
766
767         :param app: The application.
768         :type app: Flask
769         """
770
771         def _generate_plotting_area(figs: tuple, url: str) -> tuple:
772             """Generate the plotting area with all its content.
773
774             :param figs: Figures to be placed in the plotting area.
775             :param utl: The URL to be placed in the plotting area bellow the
776                 tables.
777             :type figs: tuple of plotly.graph_objects.Figure
778             :type url: str
779             :returns: tuple of elements to be shown in the plotting area.
780             :rtype: tuple(dcc.Graph, dcc.Graph, list(dbc.Col, dbc.Col))
781             """
782
783             (fig_tput, fig_lat) = figs
784
785             row_fig_tput = C.PLACEHOLDER
786             row_fig_lat = C.PLACEHOLDER
787             row_btn_dwnld = C.PLACEHOLDER
788
789             if fig_tput:
790                 row_fig_tput = [
791                     dcc.Graph(
792                         id={"type": "graph", "index": "tput"},
793                         figure=fig_tput
794                     )
795                 ]
796                 row_btn_dwnld = [
797                     dbc.Col(  # Download
798                         width=2,
799                         children=[
800                             dcc.Loading(children=[
801                                 dbc.Button(
802                                     id="btn-download-data",
803                                     children=show_tooltip(self._tooltips,
804                                         "help-download", "Download Data"),
805                                     class_name="me-1",
806                                     color="info"
807                                 ),
808                                 dcc.Download(id="download-data")
809                             ]),
810                         ]
811                     ),
812                     dbc.Col(  # Show URL
813                         width=10,
814                         children=[
815                             dbc.InputGroup(
816                                 class_name="me-1",
817                                 children=[
818                                     dbc.InputGroupText(
819                                         style=C.URL_STYLE,
820                                         children=show_tooltip(self._tooltips,
821                                             "help-url", "URL", "input-url")
822                                     ),
823                                     dbc.Input(
824                                         id="input-url",
825                                         readonly=True,
826                                         type="url",
827                                         style=C.URL_STYLE,
828                                         value=url
829                                     )
830                                 ]
831                             )
832                         ]
833                     )
834                 ]
835             if fig_lat:
836                 row_fig_lat = [
837                     dcc.Graph(
838                         id={"type": "graph", "index": "lat"},
839                         figure=fig_lat
840                     )
841                 ]
842
843             return row_fig_tput, row_fig_lat, row_btn_dwnld
844
845         @app.callback(
846             Output("control-panel", "data"),  # Store
847             Output("selected-tests", "data"),  # Store
848             Output("row-graph-tput", "children"),
849             Output("row-graph-lat", "children"),
850             Output("row-btn-download", "children"),
851             Output("row-card-sel-tests", "style"),
852             Output("row-btns-sel-tests", "style"),
853             Output("dd-ctrl-dut", "value"),
854             Output("dd-ctrl-phy", "options"),
855             Output("dd-ctrl-phy", "disabled"),
856             Output("dd-ctrl-phy", "value"),
857             Output("dd-ctrl-area", "options"),
858             Output("dd-ctrl-area", "disabled"),
859             Output("dd-ctrl-area", "value"),
860             Output("dd-ctrl-test", "options"),
861             Output("dd-ctrl-test", "disabled"),
862             Output("dd-ctrl-test", "value"),
863             Output("cl-ctrl-core", "options"),
864             Output("cl-ctrl-core", "value"),
865             Output("cl-ctrl-core-all", "value"),
866             Output("cl-ctrl-core-all", "options"),
867             Output("cl-ctrl-framesize", "options"),
868             Output("cl-ctrl-framesize", "value"),
869             Output("cl-ctrl-framesize-all", "value"),
870             Output("cl-ctrl-framesize-all", "options"),
871             Output("cl-ctrl-testtype", "options"),
872             Output("cl-ctrl-testtype", "value"),
873             Output("cl-ctrl-testtype-all", "value"),
874             Output("cl-ctrl-testtype-all", "options"),
875             Output("btn-ctrl-add", "disabled"),
876             Output("cl-ctrl-normalize", "value"),
877             Output("cl-selected", "options"),  # User selection
878             State("control-panel", "data"),  # Store
879             State("selected-tests", "data"),  # Store
880             State("cl-selected", "value"),  # User selection
881             Input("dd-ctrl-dut", "value"),
882             Input("dd-ctrl-phy", "value"),
883             Input("dd-ctrl-area", "value"),
884             Input("dd-ctrl-test", "value"),
885             Input("cl-ctrl-core", "value"),
886             Input("cl-ctrl-core-all", "value"),
887             Input("cl-ctrl-framesize", "value"),
888             Input("cl-ctrl-framesize-all", "value"),
889             Input("cl-ctrl-testtype", "value"),
890             Input("cl-ctrl-testtype-all", "value"),
891             Input("cl-ctrl-normalize", "value"),
892             Input("btn-ctrl-add", "n_clicks"),
893             Input("dpr-period", "start_date"),
894             Input("dpr-period", "end_date"),
895             Input("btn-sel-remove", "n_clicks"),
896             Input("btn-sel-remove-all", "n_clicks"),
897             Input("url", "href")
898         )
899         def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
900             dd_dut: str, dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
901             cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
902             cl_testtype: list, cl_testtype_all: list, cl_normalize: list,
903             btn_add: int, d_start: str, d_end: str, btn_remove: int,
904             btn_remove_all: int, href: str) -> tuple:
905             """Update the application when the event is detected.
906
907             :param cp_data: Current status of the control panel stored in
908                 browser.
909             :param store_sel: List of tests selected by user stored in the
910                 browser.
911             :param list_sel: List of tests selected by the user shown in the
912                 checklist.
913             :param dd_dut: Input - DUTs.
914             :param dd_phy: Input - topo- arch-nic-driver.
915             :param dd_area: Input - Tested area.
916             :param dd_test: Input - Test.
917             :param cl_core: Input - Number of cores.
918             :param cl_core_all: Input - All numbers of cores.
919             :param cl_framesize: Input - Frame sizes.
920             :param cl_framesize_all: Input - All frame sizes.
921             :param cl_testtype: Input - Test type (NDR, PDR, MRR).
922             :param cl_testtype_all: Input - All test types.
923             :param cl_normalize: Input - Normalize the results.
924             :param btn_add: Input - Button "Add Selected" tests.
925             :param d_start: Date and time where the data processing starts.
926             :param d_end: Date and time where the data processing ends.
927             :param btn_remove: Input - Button "Remove selected" tests.
928             :param btn_remove_all: Input - Button "Remove All" tests.
929             :param href: Input - The URL provided by the browser.
930             :type cp_data: dict
931             :type store_sel: list
932             :type list_sel: list
933             :type dd_dut: str
934             :type dd_phy: str
935             :type dd_area: str
936             :type dd_test: str
937             :type cl_core: list
938             :type cl_core_all: list
939             :type cl_framesize: list
940             :type cl_framesize_all: list
941             :type cl_testtype: list
942             :type cl_testtype_all: list
943             :type cl_normalize: list
944             :type btn_add: int
945             :type d_start: str
946             :type d_end: str
947             :type btn_remove: int
948             :type btn_remove_all: int
949             :type href: str
950             :returns: New values for web page elements.
951             :rtype: tuple
952             """
953
954             ctrl_panel = self.ControlPanel(cp_data)
955
956             d_start = get_date(d_start)
957             d_end = get_date(d_end)
958
959             # Parse the url:
960             parsed_url = url_decode(href)
961
962             row_fig_tput = no_update
963             row_fig_lat = no_update
964             row_btn_dwnld = no_update
965             row_card_sel_tests = no_update
966             row_btns_sel_tests = no_update
967
968             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
969
970             if trigger_id == "dd-ctrl-dut":
971                 try:
972                     dut = self.spec_tbs[dd_dut]
973                     options = sorted(
974                         [{"label": v, "value": v}for v in dut.keys()],
975                         key=lambda d: d["label"]
976                     )
977                     disabled = False
978                 except KeyError:
979                     options = list()
980                     disabled = True
981                 ctrl_panel.set({
982                     "dd-ctrl-dut-value": dd_dut,
983                     "dd-ctrl-phy-value": str(),
984                     "dd-ctrl-phy-options": options,
985                     "dd-ctrl-phy-disabled": disabled,
986                     "dd-ctrl-area-value": str(),
987                     "dd-ctrl-area-options": list(),
988                     "dd-ctrl-area-disabled": True,
989                     "dd-ctrl-test-value": str(),
990                     "dd-ctrl-test-options": list(),
991                     "dd-ctrl-test-disabled": True,
992                     "cl-ctrl-core-options": list(),
993                     "cl-ctrl-core-value": list(),
994                     "cl-ctrl-core-all-value": list(),
995                     "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
996                     "cl-ctrl-framesize-options": list(),
997                     "cl-ctrl-framesize-value": list(),
998                     "cl-ctrl-framesize-all-value": list(),
999                     "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
1000                     "cl-ctrl-testtype-options": list(),
1001                     "cl-ctrl-testtype-value": list(),
1002                     "cl-ctrl-testtype-all-value": list(),
1003                     "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
1004                 })
1005             elif trigger_id == "dd-ctrl-phy":
1006                 try:
1007                     dut = ctrl_panel.get("dd-ctrl-dut-value")
1008                     phy = self.spec_tbs[dut][dd_phy]
1009                     options = sorted(
1010                         [{"label": label(v), "value": v} for v in phy.keys()],
1011                         key=lambda d: d["label"]
1012                     )
1013                     disabled = False
1014                 except KeyError:
1015                     options = list()
1016                     disabled = True
1017                 ctrl_panel.set({
1018                     "dd-ctrl-phy-value": dd_phy,
1019                     "dd-ctrl-area-value": str(),
1020                     "dd-ctrl-area-options": options,
1021                     "dd-ctrl-area-disabled": disabled,
1022                     "dd-ctrl-test-value": str(),
1023                     "dd-ctrl-test-options": list(),
1024                     "dd-ctrl-test-disabled": True,
1025                     "cl-ctrl-core-options": list(),
1026                     "cl-ctrl-core-value": list(),
1027                     "cl-ctrl-core-all-value": list(),
1028                     "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
1029                     "cl-ctrl-framesize-options": list(),
1030                     "cl-ctrl-framesize-value": list(),
1031                     "cl-ctrl-framesize-all-value": list(),
1032                     "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
1033                     "cl-ctrl-testtype-options": list(),
1034                     "cl-ctrl-testtype-value": list(),
1035                     "cl-ctrl-testtype-all-value": list(),
1036                     "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
1037                 })
1038             elif trigger_id == "dd-ctrl-area":
1039                 try:
1040                     dut = ctrl_panel.get("dd-ctrl-dut-value")
1041                     phy = ctrl_panel.get("dd-ctrl-phy-value")
1042                     area = self.spec_tbs[dut][phy][dd_area]
1043                     options = sorted(
1044                         [{"label": v, "value": v} for v in area.keys()],
1045                         key=lambda d: d["label"]
1046                     )
1047                     disabled = False
1048                 except KeyError:
1049                     options = list()
1050                     disabled = True
1051                 ctrl_panel.set({
1052                     "dd-ctrl-area-value": dd_area,
1053                     "dd-ctrl-test-value": str(),
1054                     "dd-ctrl-test-options": options,
1055                     "dd-ctrl-test-disabled": disabled,
1056                     "cl-ctrl-core-options": list(),
1057                     "cl-ctrl-core-value": list(),
1058                     "cl-ctrl-core-all-value": list(),
1059                     "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
1060                     "cl-ctrl-framesize-options": list(),
1061                     "cl-ctrl-framesize-value": list(),
1062                     "cl-ctrl-framesize-all-value": list(),
1063                     "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
1064                     "cl-ctrl-testtype-options": list(),
1065                     "cl-ctrl-testtype-value": list(),
1066                     "cl-ctrl-testtype-all-value": list(),
1067                     "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
1068                 })
1069             elif trigger_id == "dd-ctrl-test":
1070                 core_opts = list()
1071                 framesize_opts = list()
1072                 testtype_opts = list()
1073                 dut = ctrl_panel.get("dd-ctrl-dut-value")
1074                 phy = ctrl_panel.get("dd-ctrl-phy-value")
1075                 area = ctrl_panel.get("dd-ctrl-area-value")
1076                 test = self.spec_tbs[dut][phy][area][dd_test]
1077                 cores = test["core"]
1078                 fsizes = test["frame-size"]
1079                 ttypes = test["test-type"]
1080                 if dut and phy and area and dd_test:
1081                     core_opts = [{"label": v, "value": v}
1082                         for v in sorted(cores)]
1083                     framesize_opts = [{"label": v, "value": v}
1084                         for v in sorted(fsizes)]
1085                     testtype_opts = [{"label": v, "value": v}
1086                         for v in sorted(ttypes)]
1087                     ctrl_panel.set({
1088                         "dd-ctrl-test-value": dd_test,
1089                         "cl-ctrl-core-options": core_opts,
1090                         "cl-ctrl-core-value": list(),
1091                         "cl-ctrl-core-all-value": list(),
1092                         "cl-ctrl-core-all-options": C.CL_ALL_ENABLED,
1093                         "cl-ctrl-framesize-options": framesize_opts,
1094                         "cl-ctrl-framesize-value": list(),
1095                         "cl-ctrl-framesize-all-value": list(),
1096                         "cl-ctrl-framesize-all-options": C.CL_ALL_ENABLED,
1097                         "cl-ctrl-testtype-options": testtype_opts,
1098                         "cl-ctrl-testtype-value": list(),
1099                         "cl-ctrl-testtype-all-value": list(),
1100                         "cl-ctrl-testtype-all-options": C.CL_ALL_ENABLED,
1101                     })
1102             elif trigger_id == "cl-ctrl-core":
1103                 val_sel, val_all = sync_checklists(
1104                     options=ctrl_panel.get("cl-ctrl-core-options"),
1105                     sel=cl_core,
1106                     all=list(),
1107                     id=""
1108                 )
1109                 ctrl_panel.set({
1110                     "cl-ctrl-core-value": val_sel,
1111                     "cl-ctrl-core-all-value": val_all,
1112                 })
1113             elif trigger_id == "cl-ctrl-core-all":
1114                 val_sel, val_all = sync_checklists(
1115                     options = ctrl_panel.get("cl-ctrl-core-options"),
1116                     sel=list(),
1117                     all=cl_core_all,
1118                     id="all"
1119                 )
1120                 ctrl_panel.set({
1121                     "cl-ctrl-core-value": val_sel,
1122                     "cl-ctrl-core-all-value": val_all,
1123                 })
1124             elif trigger_id == "cl-ctrl-framesize":
1125                 val_sel, val_all = sync_checklists(
1126                     options = ctrl_panel.get("cl-ctrl-framesize-options"),
1127                     sel=cl_framesize,
1128                     all=list(),
1129                     id=""
1130                 )
1131                 ctrl_panel.set({
1132                     "cl-ctrl-framesize-value": val_sel,
1133                     "cl-ctrl-framesize-all-value": val_all,
1134                 })
1135             elif trigger_id == "cl-ctrl-framesize-all":
1136                 val_sel, val_all = sync_checklists(
1137                     options = ctrl_panel.get("cl-ctrl-framesize-options"),
1138                     sel=list(),
1139                     all=cl_framesize_all,
1140                     id="all"
1141                 )
1142                 ctrl_panel.set({
1143                     "cl-ctrl-framesize-value": val_sel,
1144                     "cl-ctrl-framesize-all-value": val_all,
1145                 })
1146             elif trigger_id == "cl-ctrl-testtype":
1147                 val_sel, val_all = sync_checklists(
1148                     options = ctrl_panel.get("cl-ctrl-testtype-options"),
1149                     sel=cl_testtype,
1150                     all=list(),
1151                     id=""
1152                 )
1153                 ctrl_panel.set({
1154                     "cl-ctrl-testtype-value": val_sel,
1155                     "cl-ctrl-testtype-all-value": val_all,
1156                 })
1157             elif trigger_id == "cl-ctrl-testtype-all":
1158                 val_sel, val_all = sync_checklists(
1159                     options = ctrl_panel.get("cl-ctrl-testtype-options"),
1160                     sel=list(),
1161                     all=cl_testtype_all,
1162                     id="all"
1163                 )
1164                 ctrl_panel.set({
1165                     "cl-ctrl-testtype-value": val_sel,
1166                     "cl-ctrl-testtype-all-value": val_all,
1167                 })
1168             elif trigger_id == "btn-ctrl-add":
1169                 _ = btn_add
1170                 dut = ctrl_panel.get("dd-ctrl-dut-value")
1171                 phy = ctrl_panel.get("dd-ctrl-phy-value")
1172                 area = ctrl_panel.get("dd-ctrl-area-value")
1173                 test = ctrl_panel.get("dd-ctrl-test-value")
1174                 cores = ctrl_panel.get("cl-ctrl-core-value")
1175                 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
1176                 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
1177                 # Add selected test to the list of tests in store:
1178                 if all((dut, phy, area, test, cores, framesizes, testtypes)):
1179                     if store_sel is None:
1180                         store_sel = list()
1181                     for core in cores:
1182                         for framesize in framesizes:
1183                             for ttype in testtypes:
1184                                 if dut == "trex":
1185                                     core = str()
1186                                 tid = "-".join((
1187                                     dut, phy.replace('af_xdp', 'af-xdp'), area,
1188                                     framesize.lower(), core.lower(), test,
1189                                     ttype.lower()
1190                                 ))
1191                                 if tid not in [itm["id"] for itm in store_sel]:
1192                                     store_sel.append({
1193                                         "id": tid,
1194                                         "dut": dut,
1195                                         "phy": phy,
1196                                         "area": area,
1197                                         "test": test,
1198                                         "framesize": framesize.lower(),
1199                                         "core": core.lower(),
1200                                         "testtype": ttype.lower()
1201                                     })
1202                     store_sel = sorted(store_sel, key=lambda d: d["id"])
1203                     row_card_sel_tests = C.STYLE_ENABLED
1204                     row_btns_sel_tests = C.STYLE_ENABLED
1205                     if C.CLEAR_ALL_INPUTS:
1206                         ctrl_panel.set(ctrl_panel.defaults)
1207             elif trigger_id == "btn-sel-remove-all":
1208                 _ = btn_remove_all
1209                 row_fig_tput = C.PLACEHOLDER
1210                 row_fig_lat = C.PLACEHOLDER
1211                 row_btn_dwnld = C.PLACEHOLDER
1212                 row_card_sel_tests = C.STYLE_DISABLED
1213                 row_btns_sel_tests = C.STYLE_DISABLED
1214                 store_sel = list()
1215                 ctrl_panel.set({"cl-selected-options": list()})
1216             elif trigger_id == "btn-sel-remove":
1217                 _ = btn_remove
1218                 if list_sel:
1219                     new_store_sel = list()
1220                     for item in store_sel:
1221                         if item["id"] not in list_sel:
1222                             new_store_sel.append(item)
1223                     store_sel = new_store_sel
1224             elif trigger_id == "url":
1225                 # TODO: Add verification
1226                 url_params = parsed_url["params"]
1227                 if url_params:
1228                     store_sel = literal_eval(
1229                         url_params.get("store_sel", list())[0])
1230                     d_start = get_date(url_params.get("start", list())[0])
1231                     d_end = get_date(url_params.get("end", list())[0])
1232                     if store_sel:
1233                         row_card_sel_tests = C.STYLE_ENABLED
1234                         row_btns_sel_tests = C.STYLE_ENABLED
1235
1236             if trigger_id in ("btn-ctrl-add", "url", "dpr-period",
1237                     "btn-sel-remove", "cl-ctrl-normalize"):
1238                 if store_sel:
1239                     row_fig_tput, row_fig_lat, row_btn_dwnld = \
1240                         _generate_plotting_area(
1241                             graph_trending(self.data, store_sel, self.layout,
1242                                 d_start, d_end, bool(cl_normalize)),
1243                             gen_new_url(
1244                                 parsed_url,
1245                                 {
1246                                     "store_sel": store_sel,
1247                                     "start": d_start,
1248                                     "end": d_end
1249                                 }
1250                             )
1251                         )
1252                     ctrl_panel.set({
1253                         "cl-selected-options": list_tests(store_sel)
1254                     })
1255                 else:
1256                     row_fig_tput = C.PLACEHOLDER
1257                     row_fig_lat = C.PLACEHOLDER
1258                     row_btn_dwnld = C.PLACEHOLDER
1259                     row_card_sel_tests = C.STYLE_DISABLED
1260                     row_btns_sel_tests = C.STYLE_DISABLED
1261                     store_sel = list()
1262                     ctrl_panel.set({"cl-selected-options": list()})
1263
1264             if ctrl_panel.get("cl-ctrl-core-value") and \
1265                     ctrl_panel.get("cl-ctrl-framesize-value") and \
1266                     ctrl_panel.get("cl-ctrl-testtype-value"):
1267                 disabled = False
1268             else:
1269                 disabled = True
1270             ctrl_panel.set({
1271                 "btn-ctrl-add-disabled": disabled,
1272                 "cl-normalize-value": cl_normalize
1273             })
1274
1275             ret_val = [
1276                 ctrl_panel.panel, store_sel,
1277                 row_fig_tput, row_fig_lat, row_btn_dwnld,
1278                 row_card_sel_tests, row_btns_sel_tests
1279             ]
1280             ret_val.extend(ctrl_panel.values())
1281             return ret_val
1282
1283         @app.callback(
1284             Output("metadata-tput-lat", "children"),
1285             Output("metadata-hdrh-graph", "children"),
1286             Output("offcanvas-metadata", "is_open"),
1287             Input({"type": "graph", "index": ALL}, "clickData"),
1288             prevent_initial_call=True
1289         )
1290         def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1291             """Generates the data for the offcanvas displayed when a particular
1292             point in a graph is clicked on.
1293
1294             :param graph_data: The data from the clicked point in the graph.
1295             :type graph_data: dict
1296             :returns: The data to be displayed on the offcanvas and the
1297                 information to show the offcanvas.
1298             :rtype: tuple(list, list, bool)
1299             """
1300             try:
1301                 trigger_id = loads(
1302                     callback_context.triggered[0]["prop_id"].split(".")[0]
1303                 )["index"]
1304                 idx = 0 if trigger_id == "tput" else 1
1305                 graph_data = graph_data[idx]["points"][0]
1306             except (JSONDecodeError, IndexError, KeyError, ValueError,
1307                     TypeError):
1308                 raise PreventUpdate
1309
1310             metadata = no_update
1311             graph = list()
1312
1313             children = [
1314                 dbc.ListGroupItem(
1315                     [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1316                 ) for x in graph_data.get("text", "").split("<br>")
1317             ]
1318             if trigger_id == "tput":
1319                 title = "Throughput"
1320             elif trigger_id == "lat":
1321                 title = "Latency"
1322                 hdrh_data = graph_data.get("customdata", None)
1323                 if hdrh_data:
1324                     graph = [dbc.Card(
1325                         class_name="gy-2 p-0",
1326                         children=[
1327                             dbc.CardHeader(hdrh_data.pop("name")),
1328                             dbc.CardBody(children=[
1329                                 dcc.Graph(
1330                                     id="hdrh-latency-graph",
1331                                     figure=graph_hdrh_latency(
1332                                         hdrh_data, self.layout
1333                                     )
1334                                 )
1335                             ])
1336                         ])
1337                     ]
1338             metadata = [
1339                 dbc.Card(
1340                     class_name="gy-2 p-0",
1341                     children=[
1342                         dbc.CardHeader(children=[
1343                             dcc.Clipboard(
1344                                 target_id="tput-lat-metadata",
1345                                 title="Copy",
1346                                 style={"display": "inline-block"}
1347                             ),
1348                             title
1349                         ]),
1350                         dbc.CardBody(
1351                             id="tput-lat-metadata",
1352                             class_name="p-0",
1353                             children=[dbc.ListGroup(children, flush=True), ]
1354                         )
1355                     ]
1356                 )
1357             ]
1358
1359             return metadata, graph, True
1360
1361         @app.callback(
1362             Output("download-data", "data"),
1363             State("selected-tests", "data"),
1364             Input("btn-download-data", "n_clicks"),
1365             prevent_initial_call=True
1366         )
1367         def _download_data(store_sel, n_clicks):
1368             """Download the data
1369
1370             :param store_sel: List of tests selected by user stored in the
1371                 browser.
1372             :param n_clicks: Number of clicks on the button "Download".
1373             :type store_sel: list
1374             :type n_clicks: int
1375             :returns: dict of data frame content (base64 encoded) and meta data
1376                 used by the Download component.
1377             :rtype: dict
1378             """
1379
1380             if not n_clicks:
1381                 raise PreventUpdate
1382
1383             if not store_sel:
1384                 raise PreventUpdate
1385
1386             df = pd.DataFrame()
1387             for itm in store_sel:
1388                 sel_data = select_trending_data(self.data, itm)
1389                 if sel_data is None:
1390                     continue
1391                 df = pd.concat([df, sel_data], ignore_index=True)
1392
1393             return dcc.send_data_frame(df.to_csv, "trending_data.csv")