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