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