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