CDash: Improvements in layout
[csit.git] / csit.infra.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
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     gen_new_url, generate_options
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                 className="small",
243                 children=[
244                     dbc.Row(
245                         id="row-navbar",
246                         class_name="g-0",
247                         children=[
248                             self._add_navbar(),
249                         ]
250                     ),
251                     dcc.Loading(
252                         dbc.Offcanvas(
253                             class_name="w-50",
254                             id="offcanvas-metadata",
255                             title="Throughput And Latency",
256                             placement="end",
257                             is_open=False,
258                             children=[
259                                 dbc.Row(id="metadata-tput-lat"),
260                                 dbc.Row(id="metadata-hdrh-graph"),
261                             ]
262                         )
263                     ),
264                     dbc.Row(
265                         id="row-main",
266                         class_name="g-0",
267                         children=[
268                             dcc.Store(id="selected-tests"),
269                             dcc.Store(id="control-panel"),
270                             dcc.Location(id="url", refresh=False),
271                             self._add_ctrl_col(),
272                             self._add_plotting_col(),
273                         ]
274                     )
275                 ]
276             )
277         else:
278             return html.Div(
279                 id="div-main-error",
280                 children=[
281                     dbc.Alert(
282                         [
283                             "An Error Occured",
284                         ],
285                         color="danger",
286                     ),
287                 ]
288             )
289
290     def _add_navbar(self):
291         """Add nav element with navigation panel. It is placed on the top.
292
293         :returns: Navigation bar.
294         :rtype: dbc.NavbarSimple
295         """
296         return dbc.NavbarSimple(
297             id="navbarsimple-main",
298             children=[
299                 dbc.NavItem(
300                     dbc.NavLink(
301                         "Continuous Performance Trending",
302                         disabled=True,
303                         external_link=True,
304                         href="#"
305                     )
306                 )
307             ],
308             brand=C.BRAND,
309             brand_href="/",
310             brand_external_link=True,
311             class_name="p-2",
312             fluid=True,
313         )
314
315     def _add_ctrl_col(self) -> dbc.Col:
316         """Add column with controls. It is placed on the left side.
317
318         :returns: Column with the control panel.
319         :rtype: dbc.Col
320         """
321         return dbc.Col([
322             html.Div(
323                 children=self._add_ctrl_panel(),
324                 className="sticky-top"
325             )
326         ])
327
328     def _add_plotting_col(self) -> dbc.Col:
329         """Add column with plots and tables. It is placed on the right side.
330
331         :returns: Column with tables.
332         :rtype: dbc.Col
333         """
334         return dbc.Col(
335             id="col-plotting-area",
336             children=[
337                 dcc.Loading(
338                     children=[
339                         dbc.Row(  # Throughput
340                             id="row-graph-tput",
341                             class_name="g-0 p-2",
342                             children=[
343                                 C.PLACEHOLDER
344                             ]
345                         ),
346                         dbc.Row(  # Latency
347                             id="row-graph-lat",
348                             class_name="g-0 p-2",
349                             children=[
350                                 C.PLACEHOLDER
351                             ]
352                         ),
353                         dbc.Row(  # Download
354                             id="row-btn-download",
355                             class_name="g-0 p-2",
356                             children=[
357                                 C.PLACEHOLDER
358                             ]
359                         )
360                     ]
361                 )
362             ],
363             width=9,
364         )
365
366     def _add_ctrl_panel(self) -> dbc.Row:
367         """Add control panel.
368
369         :returns: Control panel.
370         :rtype: dbc.Row
371         """
372         return [
373             dbc.Row(
374                 class_name="g-0 p-1",
375                 children=[
376                     dbc.InputGroup(
377                         [
378                             dbc.InputGroupText(
379                                 children=show_tooltip(self._tooltips,
380                                     "help-dut", "DUT")
381                             ),
382                             dbc.Select(
383                                 id="dd-ctrl-dut",
384                                 placeholder=(
385                                     "Select a Device under Test..."
386                                 ),
387                                 options=sorted(
388                                     [
389                                         {"label": k, "value": k} \
390                                             for k in self.spec_tbs.keys()
391                                     ],
392                                     key=lambda d: d["label"]
393                                 )
394                             )
395                         ],
396                         size="sm",
397                     ),
398                 ]
399             ),
400             dbc.Row(
401                 class_name="g-0 p-1",
402                 children=[
403                     dbc.InputGroup(
404                         [
405                             dbc.InputGroupText(
406                                 children=show_tooltip(self._tooltips,
407                                     "help-infra", "Infra")
408                             ),
409                             dbc.Select(
410                                 id="dd-ctrl-phy",
411                                 placeholder=(
412                                     "Select a Physical Test Bed "
413                                     "Topology..."
414                                 )
415                             )
416                         ],
417                         size="sm",
418                     ),
419                 ]
420             ),
421             dbc.Row(
422                 class_name="g-0 p-1",
423                 children=[
424                     dbc.InputGroup(
425                         [
426                             dbc.InputGroupText(
427                                 children=show_tooltip(self._tooltips,
428                                     "help-area", "Area")
429                             ),
430                             dbc.Select(
431                                 id="dd-ctrl-area",
432                                 placeholder="Select an Area...",
433                                 disabled=True,
434                             ),
435                         ],
436                         size="sm",
437                     ),
438                 ]
439             ),
440             dbc.Row(
441                 class_name="g-0 p-1",
442                 children=[
443                     dbc.InputGroup(
444                         [
445                             dbc.InputGroupText(
446                                 children=show_tooltip(self._tooltips,
447                                     "help-test", "Test")
448                             ),
449                             dbc.Select(
450                                 id="dd-ctrl-test",
451                                 placeholder="Select a Test...",
452                                 disabled=True,
453                             ),
454                         ],
455                         size="sm",
456                     ),
457                 ]
458             ),
459             dbc.Row(
460                 class_name="g-0 p-1",
461                 children=[
462                     dbc.Label(
463                         children=show_tooltip(self._tooltips,
464                             "help-framesize", "Frame Size"),
465                     ),
466                     dbc.Col(
467                         children=[
468                             dbc.Checklist(
469                                 id="cl-ctrl-framesize-all",
470                                 options=C.CL_ALL_DISABLED,
471                                 inline=True,
472                                 switch=False
473                             ),
474                         ],
475                         width=3
476                     ),
477                     dbc.Col(
478                         children=[
479                             dbc.Checklist(
480                                 id="cl-ctrl-framesize",
481                                 inline=True,
482                                 switch=False
483                             )
484                         ]
485                     )
486                 ]
487             ),
488             dbc.Row(
489                 class_name="g-0 p-1",
490                 children=[
491                     dbc.Label(
492                         children=show_tooltip(self._tooltips,
493                             "help-cores", "Number of Cores"),
494                     ),
495                     dbc.Col(
496                         children=[
497                             dbc.Checklist(
498                                 id="cl-ctrl-core-all",
499                                 options=C.CL_ALL_DISABLED,
500                                 inline=False,
501                                 switch=False
502                             )
503                         ],
504                         width=3
505                     ),
506                     dbc.Col(
507                         children=[
508                             dbc.Checklist(
509                                 id="cl-ctrl-core",
510                                 inline=True,
511                                 switch=False
512                             )
513                         ]
514                     )
515                 ]
516             ),
517             dbc.Row(
518                 class_name="g-0 p-1",
519                 children=[
520                     dbc.Label(
521                         children=show_tooltip(self._tooltips,
522                             "help-ttype", "Test Type"),
523                     ),
524                     dbc.Col(
525                         children=[
526                             dbc.Checklist(
527                                 id="cl-ctrl-testtype-all",
528                                 options=C.CL_ALL_DISABLED,
529                                 inline=True,
530                                 switch=False
531                             ),
532                         ],
533                         width=3
534                     ),
535                     dbc.Col(
536                         children=[
537                             dbc.Checklist(
538                                 id="cl-ctrl-testtype",
539                                 inline=True,
540                                 switch=False
541                             )
542                         ]
543                     )
544                 ]
545             ),
546             dbc.Row(
547                 class_name="g-0 p-1",
548                 children=[
549                     dbc.Label(
550                         children=show_tooltip(self._tooltips,
551                             "help-normalize", "Normalize"),
552                     ),
553                     dbc.Col(
554                         children=[
555                             dbc.Checklist(
556                                 id="cl-ctrl-normalize",
557                                 options=[{
558                                     "value": "normalize",
559                                     "label": (
560                                         "Normalize results to CPU "
561                                         "frequency 2GHz"
562                                     )
563                                 }],
564                                 value=[],
565                                 inline=True,
566                                 switch=False
567                             ),
568                         ]
569                     )
570                 ]
571             ),
572             dbc.Row(
573                 class_name="g-0 p-1",
574                 children=[
575                     dbc.ButtonGroup(
576                         [
577                             dbc.Button(
578                                 id="btn-ctrl-add",
579                                 children="Add Selected",
580                                 color="info"
581                             )
582                         ]
583                     )
584                 ]
585             ),
586             dbc.Row(
587                 id="row-card-sel-tests",
588                 class_name="g-0 p-1",
589                 style=C.STYLE_DISABLED,
590                 children=[
591                     dbc.Label("Selected tests"),
592                     dbc.Checklist(
593                         class_name="overflow-auto",
594                         id="cl-selected",
595                         options=[],
596                         inline=False,
597                         style={"max-height": "12em"},
598                     )
599                 ],
600             ),
601             dbc.Row(
602                 id="row-btns-sel-tests",
603                 class_name="g-0 p-1",
604                 style=C.STYLE_DISABLED,
605                 children=[
606                     dbc.ButtonGroup(
607                         children=[
608                             dbc.Button(
609                                 id="btn-sel-remove",
610                                 children="Remove Selected",
611                                 class_name="w-100",
612                                 color="info",
613                                 disabled=False
614                             ),
615                             dbc.Button(
616                                 id="btn-sel-remove-all",
617                                 children="Remove All",
618                                 class_name="w-100",
619                                 color="info",
620                                 disabled=False
621                             ),
622                         ]
623                     )
624                 ]
625             ),
626         ]
627
628     class ControlPanel:
629         """A class representing the control panel.
630         """
631
632         def __init__(self, panel: dict) -> None:
633             """Initialisation of the control pannel by default values. If
634             particular values are provided (parameter "panel") they are set
635             afterwards.
636
637             :param panel: Custom values to be set to the control panel.
638             :param default: Default values to be set to the control panel.
639             :type panel: dict
640             :type defaults: dict
641             """
642
643             # Defines also the order of keys
644             self._defaults = {
645                 "dd-ctrl-dut-value": str(),
646                 "dd-ctrl-phy-options": list(),
647                 "dd-ctrl-phy-disabled": True,
648                 "dd-ctrl-phy-value": str(),
649                 "dd-ctrl-area-options": list(),
650                 "dd-ctrl-area-disabled": True,
651                 "dd-ctrl-area-value": str(),
652                 "dd-ctrl-test-options": list(),
653                 "dd-ctrl-test-disabled": True,
654                 "dd-ctrl-test-value": str(),
655                 "cl-ctrl-core-options": list(),
656                 "cl-ctrl-core-value": list(),
657                 "cl-ctrl-core-all-value": list(),
658                 "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
659                 "cl-ctrl-framesize-options": list(),
660                 "cl-ctrl-framesize-value": list(),
661                 "cl-ctrl-framesize-all-value": list(),
662                 "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
663                 "cl-ctrl-testtype-options": list(),
664                 "cl-ctrl-testtype-value": list(),
665                 "cl-ctrl-testtype-all-value": list(),
666                 "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
667                 "btn-ctrl-add-disabled": True,
668                 "cl-normalize-value": list(),
669                 "cl-selected-options": list()
670             }
671
672             self._panel = deepcopy(self._defaults)
673             if panel:
674                 for key in self._defaults:
675                     self._panel[key] = panel[key]
676
677         @property
678         def defaults(self) -> dict:
679             return self._defaults
680
681         @property
682         def panel(self) -> dict:
683             return self._panel
684
685         def set(self, kwargs: dict) -> None:
686             """Set the values of the Control panel.
687
688             :param kwargs: key - value pairs to be set.
689             :type kwargs: dict
690             :raises KeyError: If the key in kwargs is not present in the Control
691                 panel.
692             """
693             for key, val in kwargs.items():
694                 if key in self._panel:
695                     self._panel[key] = val
696                 else:
697                     raise KeyError(f"The key {key} is not defined.")
698
699         def get(self, key: str) -> any:
700             """Returns the value of a key from the Control panel.
701
702             :param key: The key which value should be returned.
703             :type key: str
704             :returns: The value of the key.
705             :rtype: any
706             :raises KeyError: If the key in kwargs is not present in the Control
707                 panel.
708             """
709             return self._panel[key]
710
711         def values(self) -> tuple:
712             """Returns the values from the Control panel as a list.
713
714             :returns: The values from the Control panel.
715             :rtype: list
716             """
717             return tuple(self._panel.values())
718
719     def callbacks(self, app):
720         """Callbacks for the whole application.
721
722         :param app: The application.
723         :type app: Flask
724         """
725
726         def _generate_plotting_area(figs: tuple, url: str) -> tuple:
727             """Generate the plotting area with all its content.
728
729             :param figs: Figures to be placed in the plotting area.
730             :param utl: The URL to be placed in the plotting area bellow the
731                 tables.
732             :type figs: tuple of plotly.graph_objects.Figure
733             :type url: str
734             :returns: tuple of elements to be shown in the plotting area.
735             :rtype: tuple(dcc.Graph, dcc.Graph, list(dbc.Col, dbc.Col))
736             """
737
738             (fig_tput, fig_lat) = figs
739
740             row_fig_tput = C.PLACEHOLDER
741             row_fig_lat = C.PLACEHOLDER
742             row_btn_dwnld = C.PLACEHOLDER
743
744             if fig_tput:
745                 row_fig_tput = [
746                     dcc.Graph(
747                         id={"type": "graph", "index": "tput"},
748                         figure=fig_tput
749                     )
750                 ]
751                 row_btn_dwnld = [
752                     dbc.Col(  # Download
753                         width=2,
754                         children=[
755                             dcc.Loading(children=[
756                                 dbc.Button(
757                                     id="btn-download-data",
758                                     children=show_tooltip(self._tooltips,
759                                         "help-download", "Download Data"),
760                                     class_name="me-1",
761                                     color="info"
762                                 ),
763                                 dcc.Download(id="download-data")
764                             ]),
765                         ]
766                     ),
767                     dbc.Col(  # Show URL
768                         width=10,
769                         children=[
770                             dbc.InputGroup(
771                                 class_name="me-1",
772                                 children=[
773                                     dbc.InputGroupText(
774                                         style=C.URL_STYLE,
775                                         children=show_tooltip(self._tooltips,
776                                             "help-url", "URL", "input-url")
777                                     ),
778                                     dbc.Input(
779                                         id="input-url",
780                                         readonly=True,
781                                         type="url",
782                                         style=C.URL_STYLE,
783                                         value=url
784                                     )
785                                 ]
786                             )
787                         ]
788                     )
789                 ]
790             if fig_lat:
791                 row_fig_lat = [
792                     dcc.Graph(
793                         id={"type": "graph", "index": "lat"},
794                         figure=fig_lat
795                     )
796                 ]
797
798             return row_fig_tput, row_fig_lat, row_btn_dwnld
799
800         @app.callback(
801             Output("control-panel", "data"),  # Store
802             Output("selected-tests", "data"),  # Store
803             Output("row-graph-tput", "children"),
804             Output("row-graph-lat", "children"),
805             Output("row-btn-download", "children"),
806             Output("row-card-sel-tests", "style"),
807             Output("row-btns-sel-tests", "style"),
808             Output("dd-ctrl-dut", "value"),
809             Output("dd-ctrl-phy", "options"),
810             Output("dd-ctrl-phy", "disabled"),
811             Output("dd-ctrl-phy", "value"),
812             Output("dd-ctrl-area", "options"),
813             Output("dd-ctrl-area", "disabled"),
814             Output("dd-ctrl-area", "value"),
815             Output("dd-ctrl-test", "options"),
816             Output("dd-ctrl-test", "disabled"),
817             Output("dd-ctrl-test", "value"),
818             Output("cl-ctrl-core", "options"),
819             Output("cl-ctrl-core", "value"),
820             Output("cl-ctrl-core-all", "value"),
821             Output("cl-ctrl-core-all", "options"),
822             Output("cl-ctrl-framesize", "options"),
823             Output("cl-ctrl-framesize", "value"),
824             Output("cl-ctrl-framesize-all", "value"),
825             Output("cl-ctrl-framesize-all", "options"),
826             Output("cl-ctrl-testtype", "options"),
827             Output("cl-ctrl-testtype", "value"),
828             Output("cl-ctrl-testtype-all", "value"),
829             Output("cl-ctrl-testtype-all", "options"),
830             Output("btn-ctrl-add", "disabled"),
831             Output("cl-ctrl-normalize", "value"),
832             Output("cl-selected", "options"),  # User selection
833             State("control-panel", "data"),  # Store
834             State("selected-tests", "data"),  # Store
835             State("cl-selected", "value"),  # User selection
836             Input("dd-ctrl-dut", "value"),
837             Input("dd-ctrl-phy", "value"),
838             Input("dd-ctrl-area", "value"),
839             Input("dd-ctrl-test", "value"),
840             Input("cl-ctrl-core", "value"),
841             Input("cl-ctrl-core-all", "value"),
842             Input("cl-ctrl-framesize", "value"),
843             Input("cl-ctrl-framesize-all", "value"),
844             Input("cl-ctrl-testtype", "value"),
845             Input("cl-ctrl-testtype-all", "value"),
846             Input("cl-ctrl-normalize", "value"),
847             Input("btn-ctrl-add", "n_clicks"),
848             Input("btn-sel-remove", "n_clicks"),
849             Input("btn-sel-remove-all", "n_clicks"),
850             Input("url", "href")
851         )
852         def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
853             dd_dut: str, dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
854             cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
855             cl_testtype: list, cl_testtype_all: list, cl_normalize: list,
856             btn_add: int, btn_remove: int,
857             btn_remove_all: int, href: str) -> tuple:
858             """Update the application when the event is detected.
859
860             :param cp_data: Current status of the control panel stored in
861                 browser.
862             :param store_sel: List of tests selected by user stored in the
863                 browser.
864             :param list_sel: List of tests selected by the user shown in the
865                 checklist.
866             :param dd_dut: Input - DUTs.
867             :param dd_phy: Input - topo- arch-nic-driver.
868             :param dd_area: Input - Tested area.
869             :param dd_test: Input - Test.
870             :param cl_core: Input - Number of cores.
871             :param cl_core_all: Input - All numbers of cores.
872             :param cl_framesize: Input - Frame sizes.
873             :param cl_framesize_all: Input - All frame sizes.
874             :param cl_testtype: Input - Test type (NDR, PDR, MRR).
875             :param cl_testtype_all: Input - All test types.
876             :param cl_normalize: Input - Normalize the results.
877             :param btn_add: Input - Button "Add Selected" tests.
878             :param btn_remove: Input - Button "Remove selected" tests.
879             :param btn_remove_all: Input - Button "Remove All" tests.
880             :param href: Input - The URL provided by the browser.
881             :type cp_data: dict
882             :type store_sel: list
883             :type list_sel: list
884             :type dd_dut: str
885             :type dd_phy: str
886             :type dd_area: str
887             :type dd_test: str
888             :type cl_core: list
889             :type cl_core_all: list
890             :type cl_framesize: list
891             :type cl_framesize_all: list
892             :type cl_testtype: list
893             :type cl_testtype_all: list
894             :type cl_normalize: list
895             :type btn_add: int
896             :type btn_remove: int
897             :type btn_remove_all: int
898             :type href: str
899             :returns: New values for web page elements.
900             :rtype: tuple
901             """
902
903             ctrl_panel = self.ControlPanel(cp_data)
904             norm = cl_normalize
905
906             # Parse the url:
907             parsed_url = url_decode(href)
908             if parsed_url:
909                 url_params = parsed_url["params"]
910             else:
911                 url_params = None
912
913             row_fig_tput = no_update
914             row_fig_lat = no_update
915             row_btn_dwnld = no_update
916             row_card_sel_tests = no_update
917             row_btns_sel_tests = no_update
918
919             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
920
921             if trigger_id == "dd-ctrl-dut":
922                 try:
923                     options = \
924                         generate_options(sorted(self.spec_tbs[dd_dut].keys()))
925                     disabled = False
926                 except KeyError:
927                     options = list()
928                     disabled = True
929                 ctrl_panel.set({
930                     "dd-ctrl-dut-value": dd_dut,
931                     "dd-ctrl-phy-value": str(),
932                     "dd-ctrl-phy-options": options,
933                     "dd-ctrl-phy-disabled": disabled,
934                     "dd-ctrl-area-value": str(),
935                     "dd-ctrl-area-options": list(),
936                     "dd-ctrl-area-disabled": True,
937                     "dd-ctrl-test-value": str(),
938                     "dd-ctrl-test-options": list(),
939                     "dd-ctrl-test-disabled": True,
940                     "cl-ctrl-core-options": list(),
941                     "cl-ctrl-core-value": list(),
942                     "cl-ctrl-core-all-value": list(),
943                     "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
944                     "cl-ctrl-framesize-options": list(),
945                     "cl-ctrl-framesize-value": list(),
946                     "cl-ctrl-framesize-all-value": list(),
947                     "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
948                     "cl-ctrl-testtype-options": list(),
949                     "cl-ctrl-testtype-value": list(),
950                     "cl-ctrl-testtype-all-value": list(),
951                     "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
952                 })
953             elif trigger_id == "dd-ctrl-phy":
954                 try:
955                     dut = ctrl_panel.get("dd-ctrl-dut-value")
956                     phy = self.spec_tbs[dut][dd_phy]
957                     options = [{"label": label(v), "value": v} \
958                         for v in sorted(phy.keys())]
959                     disabled = False
960                 except KeyError:
961                     options = list()
962                     disabled = True
963                 ctrl_panel.set({
964                     "dd-ctrl-phy-value": dd_phy,
965                     "dd-ctrl-area-value": str(),
966                     "dd-ctrl-area-options": options,
967                     "dd-ctrl-area-disabled": disabled,
968                     "dd-ctrl-test-value": str(),
969                     "dd-ctrl-test-options": list(),
970                     "dd-ctrl-test-disabled": True,
971                     "cl-ctrl-core-options": list(),
972                     "cl-ctrl-core-value": list(),
973                     "cl-ctrl-core-all-value": list(),
974                     "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
975                     "cl-ctrl-framesize-options": list(),
976                     "cl-ctrl-framesize-value": list(),
977                     "cl-ctrl-framesize-all-value": list(),
978                     "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
979                     "cl-ctrl-testtype-options": list(),
980                     "cl-ctrl-testtype-value": list(),
981                     "cl-ctrl-testtype-all-value": list(),
982                     "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
983                 })
984             elif trigger_id == "dd-ctrl-area":
985                 try:
986                     dut = ctrl_panel.get("dd-ctrl-dut-value")
987                     phy = ctrl_panel.get("dd-ctrl-phy-value")
988                     area = self.spec_tbs[dut][phy][dd_area]
989                     options = generate_options(sorted(area.keys()))
990                     disabled = False
991                 except KeyError:
992                     options = list()
993                     disabled = True
994                 ctrl_panel.set({
995                     "dd-ctrl-area-value": dd_area,
996                     "dd-ctrl-test-value": str(),
997                     "dd-ctrl-test-options": options,
998                     "dd-ctrl-test-disabled": disabled,
999                     "cl-ctrl-core-options": list(),
1000                     "cl-ctrl-core-value": list(),
1001                     "cl-ctrl-core-all-value": list(),
1002                     "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
1003                     "cl-ctrl-framesize-options": list(),
1004                     "cl-ctrl-framesize-value": list(),
1005                     "cl-ctrl-framesize-all-value": list(),
1006                     "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
1007                     "cl-ctrl-testtype-options": list(),
1008                     "cl-ctrl-testtype-value": list(),
1009                     "cl-ctrl-testtype-all-value": list(),
1010                     "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
1011                 })
1012             elif trigger_id == "dd-ctrl-test":
1013                 dut = ctrl_panel.get("dd-ctrl-dut-value")
1014                 phy = ctrl_panel.get("dd-ctrl-phy-value")
1015                 area = ctrl_panel.get("dd-ctrl-area-value")
1016                 if all((dut, phy, area, dd_test, )):
1017                     test = self.spec_tbs[dut][phy][area][dd_test]
1018                     ctrl_panel.set({
1019                         "dd-ctrl-test-value": dd_test,
1020                         "cl-ctrl-core-options": \
1021                             generate_options(sorted(test["core"])),
1022                         "cl-ctrl-core-value": list(),
1023                         "cl-ctrl-core-all-value": list(),
1024                         "cl-ctrl-core-all-options": C.CL_ALL_ENABLED,
1025                         "cl-ctrl-framesize-options": \
1026                             generate_options(sorted(test["frame-size"])),
1027                         "cl-ctrl-framesize-value": list(),
1028                         "cl-ctrl-framesize-all-value": list(),
1029                         "cl-ctrl-framesize-all-options": C.CL_ALL_ENABLED,
1030                         "cl-ctrl-testtype-options": \
1031                             generate_options(sorted(test["test-type"])),
1032                         "cl-ctrl-testtype-value": list(),
1033                         "cl-ctrl-testtype-all-value": list(),
1034                         "cl-ctrl-testtype-all-options": C.CL_ALL_ENABLED,
1035                     })
1036             elif trigger_id == "cl-ctrl-core":
1037                 val_sel, val_all = sync_checklists(
1038                     options=ctrl_panel.get("cl-ctrl-core-options"),
1039                     sel=cl_core,
1040                     all=list(),
1041                     id=""
1042                 )
1043                 ctrl_panel.set({
1044                     "cl-ctrl-core-value": val_sel,
1045                     "cl-ctrl-core-all-value": val_all,
1046                 })
1047             elif trigger_id == "cl-ctrl-core-all":
1048                 val_sel, val_all = sync_checklists(
1049                     options = ctrl_panel.get("cl-ctrl-core-options"),
1050                     sel=list(),
1051                     all=cl_core_all,
1052                     id="all"
1053                 )
1054                 ctrl_panel.set({
1055                     "cl-ctrl-core-value": val_sel,
1056                     "cl-ctrl-core-all-value": val_all,
1057                 })
1058             elif trigger_id == "cl-ctrl-framesize":
1059                 val_sel, val_all = sync_checklists(
1060                     options = ctrl_panel.get("cl-ctrl-framesize-options"),
1061                     sel=cl_framesize,
1062                     all=list(),
1063                     id=""
1064                 )
1065                 ctrl_panel.set({
1066                     "cl-ctrl-framesize-value": val_sel,
1067                     "cl-ctrl-framesize-all-value": val_all,
1068                 })
1069             elif trigger_id == "cl-ctrl-framesize-all":
1070                 val_sel, val_all = sync_checklists(
1071                     options = ctrl_panel.get("cl-ctrl-framesize-options"),
1072                     sel=list(),
1073                     all=cl_framesize_all,
1074                     id="all"
1075                 )
1076                 ctrl_panel.set({
1077                     "cl-ctrl-framesize-value": val_sel,
1078                     "cl-ctrl-framesize-all-value": val_all,
1079                 })
1080             elif trigger_id == "cl-ctrl-testtype":
1081                 val_sel, val_all = sync_checklists(
1082                     options = ctrl_panel.get("cl-ctrl-testtype-options"),
1083                     sel=cl_testtype,
1084                     all=list(),
1085                     id=""
1086                 )
1087                 ctrl_panel.set({
1088                     "cl-ctrl-testtype-value": val_sel,
1089                     "cl-ctrl-testtype-all-value": val_all,
1090                 })
1091             elif trigger_id == "cl-ctrl-testtype-all":
1092                 val_sel, val_all = sync_checklists(
1093                     options = ctrl_panel.get("cl-ctrl-testtype-options"),
1094                     sel=list(),
1095                     all=cl_testtype_all,
1096                     id="all"
1097                 )
1098                 ctrl_panel.set({
1099                     "cl-ctrl-testtype-value": val_sel,
1100                     "cl-ctrl-testtype-all-value": val_all,
1101                 })
1102             elif trigger_id == "btn-ctrl-add":
1103                 _ = btn_add
1104                 dut = ctrl_panel.get("dd-ctrl-dut-value")
1105                 phy = ctrl_panel.get("dd-ctrl-phy-value")
1106                 area = ctrl_panel.get("dd-ctrl-area-value")
1107                 test = ctrl_panel.get("dd-ctrl-test-value")
1108                 cores = ctrl_panel.get("cl-ctrl-core-value")
1109                 framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
1110                 testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
1111                 # Add selected test to the list of tests in store:
1112                 if all((dut, phy, area, test, cores, framesizes, testtypes)):
1113                     if store_sel is None:
1114                         store_sel = list()
1115                     for core in cores:
1116                         for framesize in framesizes:
1117                             for ttype in testtypes:
1118                                 if dut == "trex":
1119                                     core = str()
1120                                 tid = "-".join((
1121                                     dut, phy.replace('af_xdp', 'af-xdp'), area,
1122                                     framesize.lower(), core.lower(), test,
1123                                     ttype.lower()
1124                                 ))
1125                                 if tid not in [itm["id"] for itm in store_sel]:
1126                                     store_sel.append({
1127                                         "id": tid,
1128                                         "dut": dut,
1129                                         "phy": phy,
1130                                         "area": area,
1131                                         "test": test,
1132                                         "framesize": framesize.lower(),
1133                                         "core": core.lower(),
1134                                         "testtype": ttype.lower()
1135                                     })
1136                     store_sel = sorted(store_sel, key=lambda d: d["id"])
1137                     row_card_sel_tests = C.STYLE_ENABLED
1138                     row_btns_sel_tests = C.STYLE_ENABLED
1139                     if C.CLEAR_ALL_INPUTS:
1140                         ctrl_panel.set(ctrl_panel.defaults)
1141             elif trigger_id == "btn-sel-remove-all":
1142                 _ = btn_remove_all
1143                 row_fig_tput = C.PLACEHOLDER
1144                 row_fig_lat = C.PLACEHOLDER
1145                 row_btn_dwnld = C.PLACEHOLDER
1146                 row_card_sel_tests = C.STYLE_DISABLED
1147                 row_btns_sel_tests = C.STYLE_DISABLED
1148                 store_sel = list()
1149                 ctrl_panel.set({"cl-selected-options": list()})
1150             elif trigger_id == "btn-sel-remove":
1151                 _ = btn_remove
1152                 if list_sel:
1153                     new_store_sel = list()
1154                     for item in store_sel:
1155                         if item["id"] not in list_sel:
1156                             new_store_sel.append(item)
1157                     store_sel = new_store_sel
1158             elif trigger_id == "url":
1159                 if url_params:
1160                     try:
1161                         store_sel = literal_eval(url_params["store_sel"][0])
1162                         norm = literal_eval(url_params["norm"][0])
1163                     except (KeyError, IndexError):
1164                         pass
1165                     if store_sel:
1166                         row_card_sel_tests = C.STYLE_ENABLED
1167                         row_btns_sel_tests = C.STYLE_ENABLED
1168                         last_test = store_sel[-1]
1169                         test = self.spec_tbs[last_test["dut"]]\
1170                             [last_test["phy"]][last_test["area"]]\
1171                                 [last_test["test"]]
1172                         ctrl_panel.set({
1173                             "dd-ctrl-dut-value": last_test["dut"],
1174                             "dd-ctrl-phy-value": last_test["phy"],
1175                             "dd-ctrl-phy-options": generate_options(sorted(
1176                                 self.spec_tbs[last_test["dut"]].keys())),
1177                             "dd-ctrl-phy-disabled": False,
1178                             "dd-ctrl-area-value": last_test["area"],
1179                             "dd-ctrl-area-options": [
1180                                 {"label": label(v), "value": v} \
1181                                     for v in sorted(
1182                                         self.spec_tbs[last_test["dut"]]\
1183                                             [last_test["phy"]].keys())
1184                             ],
1185                             "dd-ctrl-area-disabled": False,
1186                             "dd-ctrl-test-value": last_test["test"],
1187                             "dd-ctrl-test-options": generate_options(sorted(
1188                                 self.spec_tbs[last_test["dut"]]\
1189                                     [last_test["phy"]]\
1190                                         [last_test["area"]].keys())),
1191                             "dd-ctrl-test-disabled": False,
1192                             "cl-ctrl-core-options": generate_options(sorted(
1193                                 test["core"])),
1194                             "cl-ctrl-core-value": [last_test["core"].upper(), ],
1195                             "cl-ctrl-core-all-value": list(),
1196                             "cl-ctrl-core-all-options": C.CL_ALL_ENABLED,
1197                             "cl-ctrl-framesize-options": generate_options(
1198                                 sorted(test["frame-size"])),
1199                             "cl-ctrl-framesize-value": \
1200                                 [last_test["framesize"].upper(), ],
1201                             "cl-ctrl-framesize-all-value": list(),
1202                             "cl-ctrl-framesize-all-options": C.CL_ALL_ENABLED,
1203                             "cl-ctrl-testtype-options": generate_options(sorted(
1204                                 test["test-type"])),
1205                             "cl-ctrl-testtype-value": \
1206                                 [last_test["testtype"].upper(), ],
1207                             "cl-ctrl-testtype-all-value": list(),
1208                             "cl-ctrl-testtype-all-options": C.CL_ALL_ENABLED
1209                         })
1210
1211             if trigger_id in ("btn-ctrl-add", "url", "btn-sel-remove",
1212                     "cl-ctrl-normalize"):
1213                 if store_sel:
1214                     row_fig_tput, row_fig_lat, row_btn_dwnld = \
1215                         _generate_plotting_area(
1216                             graph_trending(self.data, store_sel, self.layout,
1217                                 bool(norm)),
1218                             gen_new_url(
1219                                 parsed_url,
1220                                 {
1221                                     "store_sel": store_sel,
1222                                     "norm": norm
1223                                 }
1224                             )
1225                         )
1226                     ctrl_panel.set({
1227                         "cl-selected-options": list_tests(store_sel)
1228                     })
1229                 else:
1230                     row_fig_tput = C.PLACEHOLDER
1231                     row_fig_lat = C.PLACEHOLDER
1232                     row_btn_dwnld = C.PLACEHOLDER
1233                     row_card_sel_tests = C.STYLE_DISABLED
1234                     row_btns_sel_tests = C.STYLE_DISABLED
1235                     store_sel = list()
1236                     ctrl_panel.set({"cl-selected-options": list()})
1237
1238             if ctrl_panel.get("cl-ctrl-core-value") and \
1239                     ctrl_panel.get("cl-ctrl-framesize-value") and \
1240                     ctrl_panel.get("cl-ctrl-testtype-value"):
1241                 disabled = False
1242             else:
1243                 disabled = True
1244             ctrl_panel.set({
1245                 "btn-ctrl-add-disabled": disabled,
1246                 "cl-normalize-value": norm
1247             })
1248
1249             ret_val = [
1250                 ctrl_panel.panel, store_sel,
1251                 row_fig_tput, row_fig_lat, row_btn_dwnld,
1252                 row_card_sel_tests, row_btns_sel_tests
1253             ]
1254             ret_val.extend(ctrl_panel.values())
1255             return ret_val
1256
1257         @app.callback(
1258             Output("metadata-tput-lat", "children"),
1259             Output("metadata-hdrh-graph", "children"),
1260             Output("offcanvas-metadata", "is_open"),
1261             Input({"type": "graph", "index": ALL}, "clickData"),
1262             prevent_initial_call=True
1263         )
1264         def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1265             """Generates the data for the offcanvas displayed when a particular
1266             point in a graph is clicked on.
1267
1268             :param graph_data: The data from the clicked point in the graph.
1269             :type graph_data: dict
1270             :returns: The data to be displayed on the offcanvas and the
1271                 information to show the offcanvas.
1272             :rtype: tuple(list, list, bool)
1273             """
1274             try:
1275                 trigger_id = loads(
1276                     callback_context.triggered[0]["prop_id"].split(".")[0]
1277                 )["index"]
1278                 idx = 0 if trigger_id == "tput" else 1
1279                 graph_data = graph_data[idx]["points"][0]
1280             except (JSONDecodeError, IndexError, KeyError, ValueError,
1281                     TypeError):
1282                 raise PreventUpdate
1283
1284             metadata = no_update
1285             graph = list()
1286
1287             children = [
1288                 dbc.ListGroupItem(
1289                     [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1290                 ) for x in graph_data.get("text", "").split("<br>")
1291             ]
1292             if trigger_id == "tput":
1293                 title = "Throughput"
1294             elif trigger_id == "lat":
1295                 title = "Latency"
1296                 hdrh_data = graph_data.get("customdata", None)
1297                 if hdrh_data:
1298                     graph = [dbc.Card(
1299                         class_name="gy-2 p-0",
1300                         children=[
1301                             dbc.CardHeader(hdrh_data.pop("name")),
1302                             dbc.CardBody(children=[
1303                                 dcc.Graph(
1304                                     id="hdrh-latency-graph",
1305                                     figure=graph_hdrh_latency(
1306                                         hdrh_data, self.layout
1307                                     )
1308                                 )
1309                             ])
1310                         ])
1311                     ]
1312             metadata = [
1313                 dbc.Card(
1314                     class_name="gy-2 p-0",
1315                     children=[
1316                         dbc.CardHeader(children=[
1317                             dcc.Clipboard(
1318                                 target_id="tput-lat-metadata",
1319                                 title="Copy",
1320                                 style={"display": "inline-block"}
1321                             ),
1322                             title
1323                         ]),
1324                         dbc.CardBody(
1325                             id="tput-lat-metadata",
1326                             class_name="p-0",
1327                             children=[dbc.ListGroup(children, flush=True), ]
1328                         )
1329                     ]
1330                 )
1331             ]
1332
1333             return metadata, graph, True
1334
1335         @app.callback(
1336             Output("download-data", "data"),
1337             State("selected-tests", "data"),
1338             Input("btn-download-data", "n_clicks"),
1339             prevent_initial_call=True
1340         )
1341         def _download_data(store_sel, n_clicks):
1342             """Download the data
1343
1344             :param store_sel: List of tests selected by user stored in the
1345                 browser.
1346             :param n_clicks: Number of clicks on the button "Download".
1347             :type store_sel: list
1348             :type n_clicks: int
1349             :returns: dict of data frame content (base64 encoded) and meta data
1350                 used by the Download component.
1351             :rtype: dict
1352             """
1353
1354             if not n_clicks:
1355                 raise PreventUpdate
1356
1357             if not store_sel:
1358                 raise PreventUpdate
1359
1360             df = pd.DataFrame()
1361             for itm in store_sel:
1362                 sel_data = select_trending_data(self.data, itm)
1363                 if sel_data is None:
1364                     continue
1365                 df = pd.concat([df, sel_data], ignore_index=True)
1366
1367             return dcc.send_data_frame(df.to_csv, C.TREND_DOWNLOAD_FILE_NAME)