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