C-Dash: Add telemetry panel
[csit.git] / csit.infra.dash / app / cdash / trending / layout.py
1 # Copyright (c) 2023 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
15 """Plotly Dash HTML layout override.
16 """
17
18 import logging
19 import pandas as pd
20 import dash_bootstrap_components as dbc
21
22 from flask import Flask
23 from dash import dcc
24 from dash import html
25 from dash import callback_context, no_update, ALL
26 from dash import Input, Output, State
27 from dash.exceptions import PreventUpdate
28 from yaml import load, FullLoader, YAMLError
29 from ast import literal_eval
30 from copy import deepcopy
31
32 from ..utils.constants import Constants as C
33 from ..utils.control_panel import ControlPanel
34 from ..utils.trigger import Trigger
35 from ..utils.telemetry_data import TelemetryData
36 from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
37     generate_options, get_list_group_items
38 from ..utils.url_processing import url_decode
39 from ..data.data import Data
40 from .graphs import graph_trending, graph_hdrh_latency, select_trending_data, \
41     graph_tm_trending
42
43
44 # Control panel partameters and their default values.
45 CP_PARAMS = {
46     "dd-dut-val": str(),
47     "dd-phy-opt": list(),
48     "dd-phy-dis": True,
49     "dd-phy-val": str(),
50     "dd-area-opt": list(),
51     "dd-area-dis": True,
52     "dd-area-val": str(),
53     "dd-test-opt": list(),
54     "dd-test-dis": True,
55     "dd-test-val": str(),
56     "cl-core-opt": list(),
57     "cl-core-val": list(),
58     "cl-core-all-val": list(),
59     "cl-core-all-opt": C.CL_ALL_DISABLED,
60     "cl-frmsize-opt": list(),
61     "cl-frmsize-val": list(),
62     "cl-frmsize-all-val": list(),
63     "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
64     "cl-tsttype-opt": list(),
65     "cl-tsttype-val": list(),
66     "cl-tsttype-all-val": list(),
67     "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
68     "btn-add-dis": True,
69     "cl-normalize-val": list()
70 }
71
72
73 class Layout:
74     """The layout of the dash app and the callbacks.
75     """
76
77     def __init__(self, app: Flask, html_layout_file: str,
78         graph_layout_file: str, data_spec_file: str, tooltip_file: str,
79         time_period: str=None) -> None:
80         """Initialization:
81         - save the input parameters,
82         - read and pre-process the data,
83         - prepare data for the control panel,
84         - read HTML layout file,
85         - read tooltips from the tooltip file.
86
87         :param app: Flask application running the dash application.
88         :param html_layout_file: Path and name of the file specifying the HTML
89             layout of the dash application.
90         :param graph_layout_file: Path and name of the file with layout of
91             plot.ly graphs.
92         :param data_spec_file: Path and name of the file specifying the data to
93             be read from parquets for this application.
94         :param tooltip_file: Path and name of the yaml file specifying the
95             tooltips.
96         :param time_period: It defines the time period for data read from the
97             parquets in days from now back to the past.
98         :type app: Flask
99         :type html_layout_file: str
100         :type graph_layout_file: str
101         :type data_spec_file: str
102         :type tooltip_file: str
103         :type time_period: int
104         """
105
106         # Inputs
107         self._app = app
108         self._html_layout_file = html_layout_file
109         self._graph_layout_file = graph_layout_file
110         self._data_spec_file = data_spec_file
111         self._tooltip_file = tooltip_file
112         self._time_period = time_period
113
114         # Read the data:
115         data_mrr = Data(
116             data_spec_file=self._data_spec_file,
117             debug=True
118         ).read_trending_mrr(days=self._time_period)
119
120         data_ndrpdr = Data(
121             data_spec_file=self._data_spec_file,
122             debug=True
123         ).read_trending_ndrpdr(days=self._time_period)
124
125         self._data = pd.concat(
126             [data_mrr, data_ndrpdr],
127             ignore_index=True,
128             copy=False
129         )
130
131         # Get structure of tests:
132         tbs = dict()
133         for _, row in self._data[["job", "test_id"]].drop_duplicates().\
134                 iterrows():
135             lst_job = row["job"].split("-")
136             dut = lst_job[1]
137             ttype = lst_job[3]
138             tbed = "-".join(lst_job[-2:])
139             lst_test = row["test_id"].split(".")
140             if dut == "dpdk":
141                 area = "dpdk"
142             else:
143                 area = "-".join(lst_test[3:-2])
144             suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\
145                 replace("2n-", "")
146             test = lst_test[-1]
147             nic = suite.split("-")[0]
148             for drv in C.DRIVERS:
149                 if drv in test:
150                     if drv == "af-xdp":
151                         driver = "af_xdp"
152                     else:
153                         driver = drv
154                     test = test.replace(f"{drv}-", "")
155                     break
156             else:
157                 driver = "dpdk"
158             infra = "-".join((tbed, nic, driver))
159             lst_test = test.split("-")
160             framesize = lst_test[0]
161             core = lst_test[1] if lst_test[1] else "8C"
162             test = "-".join(lst_test[2: -1])
163
164             if tbs.get(dut, None) is None:
165                 tbs[dut] = dict()
166             if tbs[dut].get(infra, None) is None:
167                 tbs[dut][infra] = dict()
168             if tbs[dut][infra].get(area, None) is None:
169                 tbs[dut][infra][area] = dict()
170             if tbs[dut][infra][area].get(test, None) is None:
171                 tbs[dut][infra][area][test] = dict()
172                 tbs[dut][infra][area][test]["core"] = list()
173                 tbs[dut][infra][area][test]["frame-size"] = list()
174                 tbs[dut][infra][area][test]["test-type"] = list()
175             if core.upper() not in tbs[dut][infra][area][test]["core"]:
176                 tbs[dut][infra][area][test]["core"].append(core.upper())
177             if framesize.upper() not in \
178                     tbs[dut][infra][area][test]["frame-size"]:
179                 tbs[dut][infra][area][test]["frame-size"].append(
180                     framesize.upper()
181                 )
182             if ttype == "mrr":
183                 if "MRR" not in tbs[dut][infra][area][test]["test-type"]:
184                     tbs[dut][infra][area][test]["test-type"].append("MRR")
185             elif ttype == "ndrpdr":
186                 if "NDR" not in tbs[dut][infra][area][test]["test-type"]:
187                     tbs[dut][infra][area][test]["test-type"].extend(
188                         ("NDR", "PDR")
189                     )
190         self._spec_tbs = tbs
191
192         # Read from files:
193         self._html_layout = str()
194         self._graph_layout = None
195         self._tooltips = dict()
196
197         try:
198             with open(self._html_layout_file, "r") as file_read:
199                 self._html_layout = file_read.read()
200         except IOError as err:
201             raise RuntimeError(
202                 f"Not possible to open the file {self._html_layout_file}\n{err}"
203             )
204
205         try:
206             with open(self._graph_layout_file, "r") as file_read:
207                 self._graph_layout = load(file_read, Loader=FullLoader)
208         except IOError as err:
209             raise RuntimeError(
210                 f"Not possible to open the file {self._graph_layout_file}\n"
211                 f"{err}"
212             )
213         except YAMLError as err:
214             raise RuntimeError(
215                 f"An error occurred while parsing the specification file "
216                 f"{self._graph_layout_file}\n{err}"
217             )
218
219         try:
220             with open(self._tooltip_file, "r") as file_read:
221                 self._tooltips = load(file_read, Loader=FullLoader)
222         except IOError as err:
223             logging.warning(
224                 f"Not possible to open the file {self._tooltip_file}\n{err}"
225             )
226         except YAMLError as err:
227             logging.warning(
228                 f"An error occurred while parsing the specification file "
229                 f"{self._tooltip_file}\n{err}"
230             )
231
232         # Callbacks:
233         if self._app is not None and hasattr(self, "callbacks"):
234             self.callbacks(self._app)
235
236     @property
237     def html_layout(self):
238         return self._html_layout
239
240     def add_content(self):
241         """Top level method which generated the web page.
242
243         It generates:
244         - Store for user input data,
245         - Navigation bar,
246         - Main area with control panel and ploting area.
247
248         If no HTML layout is provided, an error message is displayed instead.
249
250         :returns: The HTML div with the whole page.
251         :rtype: html.Div
252         """
253
254         if self.html_layout and self._spec_tbs:
255             return html.Div(
256                 id="div-main",
257                 className="small",
258                 children=[
259                     dcc.Store(id="store-selected-tests"),
260                     dcc.Store(id="store-control-panel"),
261                     dcc.Store(id="store-telemetry-data"),
262                     dcc.Store(id="store-telemetry-user"),
263                     dcc.Location(id="url", refresh=False),
264                     dbc.Row(
265                         id="row-navbar",
266                         class_name="g-0",
267                         children=[
268                             self._add_navbar()
269                         ]
270                     ),
271                     dbc.Row(
272                         id="row-main",
273                         class_name="g-0",
274                         children=[
275                             self._add_ctrl_col(),
276                             self._add_plotting_col()
277                         ]
278                     ),
279                     dbc.Spinner(
280                         dbc.Offcanvas(
281                             class_name="w-50",
282                             id="offcanvas-metadata",
283                             title="Throughput And Latency",
284                             placement="end",
285                             is_open=False,
286                             children=[
287                                 dbc.Row(id="metadata-tput-lat"),
288                                 dbc.Row(id="metadata-hdrh-graph")
289                             ]
290                         ),
291                         delay_show=C.SPINNER_DELAY
292                     )
293                 ]
294             )
295         else:
296             return html.Div(
297                 id="div-main-error",
298                 children=[
299                     dbc.Alert(
300                         [
301                             "An Error Occured"
302                         ],
303                         color="danger"
304                     )
305                 ]
306             )
307
308     def _add_navbar(self):
309         """Add nav element with navigation panel. It is placed on the top.
310
311         :returns: Navigation bar.
312         :rtype: dbc.NavbarSimple
313         """
314         return dbc.NavbarSimple(
315             id="navbarsimple-main",
316             children=[
317                 dbc.NavItem(
318                     dbc.NavLink(
319                         C.TREND_TITLE,
320                         disabled=True,
321                         external_link=True,
322                         href="#"
323                     )
324                 )
325             ],
326             brand=C.BRAND,
327             brand_href="/",
328             brand_external_link=True,
329             class_name="p-2",
330             fluid=True
331         )
332
333     def _add_ctrl_col(self) -> dbc.Col:
334         """Add column with controls. It is placed on the left side.
335
336         :returns: Column with the control panel.
337         :rtype: dbc.Col
338         """
339         return dbc.Col([
340             html.Div(
341                 children=self._add_ctrl_panel(),
342                 className="sticky-top"
343             )
344         ])
345
346     def _add_ctrl_panel(self) -> list:
347         """Add control panel.
348
349         :returns: Control panel.
350         :rtype: list
351         """
352         return [
353             dbc.Row(
354                 class_name="g-0 p-1",
355                 children=[
356                     dbc.InputGroup(
357                         [
358                             dbc.InputGroupText(
359                                 children=show_tooltip(
360                                     self._tooltips,
361                                     "help-dut",
362                                     "DUT"
363                                 )
364                             ),
365                             dbc.Select(
366                                 id={"type": "ctrl-dd", "index": "dut"},
367                                 placeholder="Select a Device under Test...",
368                                 options=sorted(
369                                     [
370                                         {"label": k, "value": k} \
371                                             for k in self._spec_tbs.keys()
372                                     ],
373                                     key=lambda d: d["label"]
374                                 )
375                             )
376                         ],
377                         size="sm"
378                     )
379                 ]
380             ),
381             dbc.Row(
382                 class_name="g-0 p-1",
383                 children=[
384                     dbc.InputGroup(
385                         [
386                             dbc.InputGroupText(
387                                 children=show_tooltip(
388                                     self._tooltips,
389                                     "help-infra",
390                                     "Infra"
391                                 )
392                             ),
393                             dbc.Select(
394                                 id={"type": "ctrl-dd", "index": "phy"},
395                                 placeholder=\
396                                     "Select a Physical Test Bed Topology..."
397                             )
398                         ],
399                         size="sm"
400                     )
401                 ]
402             ),
403             dbc.Row(
404                 class_name="g-0 p-1",
405                 children=[
406                     dbc.InputGroup(
407                         [
408                             dbc.InputGroupText(
409                                 children=show_tooltip(
410                                     self._tooltips,
411                                     "help-area",
412                                     "Area"
413                                 )
414                             ),
415                             dbc.Select(
416                                 id={"type": "ctrl-dd", "index": "area"},
417                                 placeholder="Select an Area..."
418                             )
419                         ],
420                         size="sm"
421                     )
422                 ]
423             ),
424             dbc.Row(
425                 class_name="g-0 p-1",
426                 children=[
427                     dbc.InputGroup(
428                         [
429                             dbc.InputGroupText(
430                                 children=show_tooltip(
431                                     self._tooltips,
432                                     "help-test",
433                                     "Test"
434                                 )
435                             ),
436                             dbc.Select(
437                                 id={"type": "ctrl-dd", "index": "test"},
438                                 placeholder="Select a Test..."
439                             )
440                         ],
441                         size="sm"
442                     )
443                 ]
444             ),
445             dbc.Row(
446                 class_name="g-0 p-1",
447                 children=[
448                     dbc.InputGroup(
449                         [
450                             dbc.InputGroupText(
451                                 children=show_tooltip(
452                                     self._tooltips,
453                                     "help-framesize",
454                                     "Frame Size"
455                                 )
456                             ),
457                             dbc.Col(
458                                 children=[
459                                     dbc.Checklist(
460                                         id={
461                                             "type": "ctrl-cl",
462                                             "index": "frmsize-all"
463                                         },
464                                         options=C.CL_ALL_DISABLED,
465                                         inline=True,
466                                         class_name="ms-2"
467                                     )
468                                 ],
469                                 width=2
470                             ),
471                             dbc.Col(
472                                 children=[
473                                     dbc.Checklist(
474                                         id={
475                                             "type": "ctrl-cl",
476                                             "index": "frmsize"
477                                         },
478                                         inline=True
479                                     )
480                                 ]
481                             )
482                         ],
483                         style={"align-items": "center"},
484                         size="sm"
485                     )
486                 ]
487             ),
488             dbc.Row(
489                 class_name="g-0 p-1",
490                 children=[
491                     dbc.InputGroup(
492                         [
493                             dbc.InputGroupText(
494                                 children=show_tooltip(
495                                     self._tooltips,
496                                     "help-cores",
497                                     "Number of Cores"
498                                 )
499                             ),
500                             dbc.Col(
501                                 children=[
502                                     dbc.Checklist(
503                                         id={
504                                             "type": "ctrl-cl",
505                                             "index": "core-all"
506                                         },
507                                         options=C.CL_ALL_DISABLED,
508                                         inline=True,
509                                         class_name="ms-2"
510                                     )
511                                 ],
512                                 width=2
513                             ),
514                             dbc.Col(
515                                 children=[
516                                     dbc.Checklist(
517                                         id={
518                                             "type": "ctrl-cl",
519                                             "index": "core"
520                                         },
521                                         inline=True
522                                     )
523                                 ]
524                             )
525                         ],
526                         style={"align-items": "center"},
527                         size="sm"
528                     )
529                 ]
530             ),
531             dbc.Row(
532                 class_name="g-0 p-1",
533                 children=[
534                     dbc.InputGroup(
535                         [
536                             dbc.InputGroupText(
537                                 children=show_tooltip(
538                                     self._tooltips,
539                                     "help-ttype",
540                                     "Test Type"
541                                 )
542                             ),
543                             dbc.Col(
544                                 children=[
545                                     dbc.Checklist(
546                                         id={
547                                             "type": "ctrl-cl",
548                                             "index": "tsttype-all"
549                                         },
550                                         options=C.CL_ALL_DISABLED,
551                                         inline=True,
552                                         class_name="ms-2"
553                                     )
554                                 ],
555                                 width=2
556                             ),
557                             dbc.Col(
558                                 children=[
559                                     dbc.Checklist(
560                                         id={
561                                             "type": "ctrl-cl",
562                                             "index": "tsttype"
563                                         },
564                                         inline=True
565                                     )
566                                 ]
567                             )
568                         ],
569                         style={"align-items": "center"},
570                         size="sm"
571                     )
572                 ]
573             ),
574             dbc.Row(
575                 class_name="g-0 p-1",
576                 children=[
577                     dbc.InputGroup(
578                         [
579                             dbc.InputGroupText(
580                                 children=show_tooltip(
581                                     self._tooltips,
582                                     "help-normalize",
583                                     "Normalization"
584                                 )
585                             ),
586                             dbc.Col(
587                                 children=[
588                                     dbc.Checklist(
589                                         id="normalize",
590                                         options=[{
591                                             "value": "normalize",
592                                             "label": (
593                                                 "Normalize to CPU frequency "
594                                                 "2GHz"
595                                             )
596                                         }],
597                                         value=[],
598                                         inline=True,
599                                         class_name="ms-2"
600                                     )
601                                 ]
602                             )
603                         ],
604                         style={"align-items": "center"},
605                         size="sm"
606                     )
607                 ]
608             ),
609             dbc.Row(
610                 class_name="g-0 p-1",
611                 children=[
612                     dbc.Button(
613                         id={"type": "ctrl-btn", "index": "add-test"},
614                         children="Add Selected",
615                         color="info"
616                     )
617                 ]
618             ),
619             dbc.Row(
620                 id="row-card-sel-tests",
621                 class_name="g-0 p-1",
622                 style=C.STYLE_DISABLED,
623                 children=[
624                     dbc.ListGroup(
625                         class_name="overflow-auto p-0",
626                         id="lg-selected",
627                         children=[],
628                         style={"max-height": "14em"},
629                         flush=True
630                     )
631                 ]
632             ),
633             dbc.Row(
634                 id="row-btns-sel-tests",
635                 class_name="g-0 p-1",
636                 style=C.STYLE_DISABLED,
637                 children=[
638                     dbc.ButtonGroup(
639                         children=[
640                             dbc.Button(
641                                 id={"type": "ctrl-btn", "index": "rm-test"},
642                                 children="Remove Selected",
643                                 class_name="w-100",
644                                 color="info",
645                                 disabled=False
646                             ),
647                             dbc.Button(
648                                 id={"type": "ctrl-btn", "index": "rm-test-all"},
649                                 children="Remove All",
650                                 class_name="w-100",
651                                 color="info",
652                                 disabled=False
653                             )
654                         ]
655                     )
656                 ]
657             )
658         ]
659
660     def _add_plotting_col(self) -> dbc.Col:
661         """Add column with plots. It is placed on the right side.
662
663         :returns: Column with plots.
664         :rtype: dbc.Col
665         """
666         return dbc.Col(
667             id="col-plotting-area",
668             children=[
669                 dbc.Spinner(
670                     dbc.Row(
671                         id="plotting-area-trending",
672                         class_name="g-0 p-0",
673                         children=C.PLACEHOLDER
674                     ),
675                     delay_show=C.SPINNER_DELAY
676                 ),
677                 dbc.Row(
678                     id="plotting-area-telemetry",
679                     class_name="g-0 p-0",
680                     children=C.PLACEHOLDER
681                 ),
682                 dbc.Row(
683                     id="plotting-area-buttons",
684                     class_name="g-0 p-0",
685                     children=C.PLACEHOLDER
686                 )
687             ],
688             width=9
689         )
690
691     def _get_plotting_area_buttons(self) -> dbc.Col:
692         """Add buttons and modals to the plotting area.
693
694         :returns: A column with buttons and modals for telemetry.
695         :rtype: dbc.Col
696         """
697         return dbc.Col([
698             html.Div(
699                 [
700                     dbc.Button(
701                         id={"type": "telemetry-btn", "index": "open"},
702                         children="Add Panel with Telemetry",
703                         class_name="me-1",
704                         color="info",
705                         style={
706                             "text-transform": "none",
707                             "padding": "0rem 1rem"
708                         }
709                     ),
710                     dbc.Modal(
711                         [
712                             dbc.ModalHeader(
713                                 dbc.ModalTitle(
714                                     "Select a Metric"
715                                 ),
716                                 close_button=False
717                             ),
718                             dbc.Spinner(
719                                 dbc.ModalBody(
720                                     id="plot-mod-telemetry-body-1",
721                                     children=self._get_telemetry_step_1()
722                                 ),
723                                 delay_show=2*C.SPINNER_DELAY
724                             ),
725                             dbc.ModalFooter([
726                                 dbc.Button(
727                                     "Select",
728                                     id={
729                                         "type": "telemetry-btn",
730                                         "index": "select"
731                                     },
732                                     disabled=True
733                                 ),
734                                 dbc.Button(
735                                     "Cancel",
736                                     id={
737                                         "type": "telemetry-btn",
738                                         "index": "cancel"
739                                     },
740                                     disabled=False
741                                 )
742                             ])
743                         ],
744                         id="plot-mod-telemetry-1",
745                         size="lg",
746                         is_open=False,
747                         scrollable=False,
748                         backdrop="static",
749                         keyboard=False
750                     ),
751                     dbc.Modal(
752                         [
753                             dbc.ModalHeader(
754                                 dbc.ModalTitle(
755                                     "Select Labels"
756                                 ),
757                                 close_button=False
758                             ),
759                             dbc.Spinner(
760                                 dbc.ModalBody(
761                                     id="plot-mod-telemetry-body-2",
762                                     children=self._get_telemetry_step_2()
763                                 ),
764                                 delay_show=2*C.SPINNER_DELAY
765                             ),
766                             dbc.ModalFooter([
767                                 dbc.Button(
768                                     "Back",
769                                     id={
770                                         "type": "telemetry-btn",
771                                         "index": "back"
772                                     },
773                                     disabled=False
774                                 ),
775                                 dbc.Button(
776                                     "Add Telemetry",
777                                     id={
778                                         "type": "telemetry-btn",
779                                         "index": "add"
780                                     },
781                                     disabled=True
782                                 ),
783                                 dbc.Button(
784                                     "Cancel",
785                                     id={
786                                         "type": "telemetry-btn",
787                                         "index": "cancel"
788                                     },
789                                     disabled=False
790                                 )
791                             ])
792                         ],
793                         id="plot-mod-telemetry-2",
794                         size="xl",
795                         is_open=False,
796                         scrollable=False,
797                         backdrop="static",
798                         keyboard=False
799                     )
800                 ],
801                 className="d-grid gap-0 d-md-flex justify-content-md-end"
802             )
803         ])
804
805     def _get_plotting_area_trending(
806             self,
807             tests: list,
808             normalize: bool,
809             url: str
810         ) -> dbc.Col:
811         """Generate the plotting area with all its content.
812
813         :param tests: A list of tests to be displayed in the trending graphs.
814         :param normalize: If True, the data in graphs is normalized.
815         :param url: An URL to be displayed in the modal window.
816         :type tests: list
817         :type normalize: bool
818         :type url: str
819         :returns: A collumn with trending graphs (tput and latency) in tabs.
820         :rtype: dbc.Col
821         """
822         if not tests:
823             return C.PLACEHOLDER
824
825         figs = graph_trending(self._data, tests, self._graph_layout, normalize)
826
827         if not figs[0]:
828             return C.PLACEHOLDER
829
830         tab_items = [
831             dbc.Tab(
832                 children=dcc.Graph(
833                     id={"type": "graph", "index": "tput"},
834                     figure=figs[0]
835                 ),
836                 label="Throughput",
837                 tab_id="tab-tput"
838             )
839         ]
840
841         if figs[1]:
842             tab_items.append(
843                 dbc.Tab(
844                     children=dcc.Graph(
845                         id={"type": "graph", "index": "lat"},
846                         figure=figs[1]
847                     ),
848                     label="Latency",
849                     tab_id="tab-lat"
850                 )
851             )
852
853         trending = [
854             dbc.Row(children=[
855                 dbc.Tabs(
856                     children=tab_items,
857                     id="tabs",
858                     active_tab="tab-tput",
859                 )
860             ]),
861             dbc.Row(
862                 [
863                     dbc.Col([html.Div(
864                         [
865                             dbc.Button(
866                                 id="plot-btn-url",
867                                 children="Show URL",
868                                 class_name="me-1",
869                                 color="info",
870                                 style={
871                                     "text-transform": "none",
872                                     "padding": "0rem 1rem"
873                                 }
874                             ),
875                             dbc.Modal(
876                                 [
877                                     dbc.ModalHeader(dbc.ModalTitle("URL")),
878                                     dbc.ModalBody(url)
879                                 ],
880                                 id="plot-mod-url",
881                                 size="xl",
882                                 is_open=False,
883                                 scrollable=True
884                             ),
885                             dbc.Button(
886                                 id="plot-btn-download",
887                                 children="Download Data",
888                                 class_name="me-1",
889                                 color="info",
890                                 style={
891                                     "text-transform": "none",
892                                     "padding": "0rem 1rem"
893                                 }
894                             ),
895                             dcc.Download(id="download-trending-data")
896                         ],
897                         className=\
898                             "d-grid gap-0 d-md-flex justify-content-md-end"
899                     )])
900                 ],
901                 class_name="g-0 p-0"
902             )
903         ]
904
905         return dbc.Col(
906             children=[
907                 dbc.Row(
908                     dbc.Accordion(
909                         children=[
910                             dbc.AccordionItem(
911                                 title="Trending",
912                                 children=trending
913                             )
914                         ],
915                         class_name="g-0 p-1",
916                         start_collapsed=False,
917                         always_open=True,
918                         active_item=["item-0", ]
919                     ),
920                     class_name="g-0 p-0",
921                 )
922             ]
923         )
924
925     def _get_plotting_area_telemetry(self, graphs: list) -> dbc.Col:
926         """Generate the plotting area with telemetry.
927         """
928         if not graphs:
929             return C.PLACEHOLDER
930         
931         acc_items = list()
932         for graph in graphs:
933             acc_items.append(
934                 dbc.AccordionItem(
935                     title=f"Telemetry: {graph[1]}",
936                     children=dcc.Graph(
937                         id={"type": "graph-telemetry", "index": graph[1]},
938                         figure=graph[0]
939                     )
940                 )
941             )
942
943         return dbc.Col(
944             children=[
945                 dbc.Row(
946                     dbc.Accordion(
947                         children=acc_items,
948                         class_name="g-0 p-1",
949                         start_collapsed=False,
950                         always_open=True,
951                         active_item=[f"item-{i}" for i in range(len(acc_items))]
952                     ),
953                     class_name="g-0 p-0",
954                 )
955             ]
956         )
957
958     @staticmethod
959     def _get_telemetry_step_1() -> list:
960         """Return the content of the modal window used in the step 1 of metrics
961         selection.
962
963         :returns: A list of dbc rows with 'input' and 'search output'.
964         :rtype: list
965         """
966         return [
967             dbc.Row(
968                 class_name="g-0 p-1",
969                 children=[
970                     dbc.Input(
971                         id="telemetry-search-in",
972                         placeholder="Start typing a metric name...",
973                         type="text"
974                     )
975                 ]
976             ),
977             dbc.Row(
978                 class_name="g-0 p-1",
979                 children=[
980                     dbc.ListGroup(
981                         class_name="overflow-auto p-0",
982                         id="telemetry-search-out",
983                         children=[],
984                         style={"max-height": "14em"},
985                         flush=True
986                     )
987                 ]
988             )
989         ]
990
991     @staticmethod
992     def _get_telemetry_step_2() -> list:
993         """Return the content of the modal window used in the step 2 of metrics
994         selection.
995
996         :returns: A list of dbc rows with 'container with dynamic dropdowns' and
997             'search output'.
998         :rtype: list
999         """
1000         return [
1001             dbc.Row(
1002                 id="telemetry-dd",
1003                 class_name="g-0 p-1",
1004                 children=["Add content here."]
1005             ),
1006             dbc.Row(
1007                 class_name="g-0 p-1",
1008                 children=[
1009                     dbc.Textarea(
1010                         id="telemetry-list-metrics",
1011                         rows=20,
1012                         size="sm",
1013                         wrap="off",
1014                         readonly=True
1015                     )
1016                 ]
1017             )
1018         ]
1019
1020     def callbacks(self, app):
1021         """Callbacks for the whole application.
1022
1023         :param app: The application.
1024         :type app: Flask
1025         """
1026
1027         @app.callback(
1028             [
1029                 Output("store-control-panel", "data"),
1030                 Output("store-selected-tests", "data"),
1031                 Output("plotting-area-trending", "children"),
1032                 Output("plotting-area-buttons", "children"),
1033                 Output("row-card-sel-tests", "style"),
1034                 Output("row-btns-sel-tests", "style"),
1035                 Output("lg-selected", "children"),
1036                 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
1037                 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
1038                 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
1039                 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
1040                 Output({"type": "ctrl-dd", "index": "area"}, "options"),
1041                 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
1042                 Output({"type": "ctrl-dd", "index": "area"}, "value"),
1043                 Output({"type": "ctrl-dd", "index": "test"}, "options"),
1044                 Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
1045                 Output({"type": "ctrl-dd", "index": "test"}, "value"),
1046                 Output({"type": "ctrl-cl", "index": "core"}, "options"),
1047                 Output({"type": "ctrl-cl", "index": "core"}, "value"),
1048                 Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
1049                 Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
1050                 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
1051                 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
1052                 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
1053                 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
1054                 Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
1055                 Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
1056                 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
1057                 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
1058                 Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
1059                 Output("normalize", "value")
1060             ],
1061             [
1062                 State("store-control-panel", "data"),
1063                 State("store-selected-tests", "data"),
1064                 State({"type": "sel-cl", "index": ALL}, "value")
1065             ],
1066             [
1067                 Input("url", "href"),
1068                 Input("normalize", "value"),
1069                 Input({"type": "ctrl-dd", "index": ALL}, "value"),
1070                 Input({"type": "ctrl-cl", "index": ALL}, "value"),
1071                 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
1072             ],
1073             prevent_initial_call=True
1074         )
1075         def _update_application(
1076                 control_panel: dict,
1077                 store_sel: list,
1078                 lst_sel: list,
1079                 href: str,
1080                 normalize: list,
1081                 *_
1082             ) -> tuple:
1083             """Update the application when the event is detected.
1084             """
1085
1086             ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
1087             on_draw = False
1088
1089             # Parse the url:
1090             parsed_url = url_decode(href)
1091             if parsed_url:
1092                 url_params = parsed_url["params"]
1093             else:
1094                 url_params = None
1095
1096             trigger = Trigger(callback_context.triggered)
1097
1098             if trigger.type == "url" and url_params:
1099                 try:
1100                     store_sel = literal_eval(url_params["store_sel"][0])
1101                     normalize = literal_eval(url_params["norm"][0])
1102                 except (KeyError, IndexError):
1103                     pass
1104                 if store_sel:
1105                     last_test = store_sel[-1]
1106                     test = self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1107                         [last_test["area"]][last_test["test"]]
1108                     ctrl_panel.set({
1109                         "dd-dut-val": last_test["dut"],
1110                         "dd-phy-val": last_test["phy"],
1111                         "dd-phy-opt": generate_options(
1112                             self._spec_tbs[last_test["dut"]].keys()
1113                         ),
1114                         "dd-phy-dis": False,
1115                         "dd-area-val": last_test["area"],
1116                         "dd-area-opt": [
1117                             {"label": label(v), "value": v} for v in sorted(
1118                                 self._spec_tbs[last_test["dut"]]\
1119                                     [last_test["phy"]].keys()
1120                             )
1121                         ],
1122                         "dd-area-dis": False,
1123                         "dd-test-val": last_test["test"],
1124                         "dd-test-opt": generate_options(
1125                             self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1126                                 [last_test["area"]].keys()
1127                         ),
1128                         "dd-test-dis": False,
1129                         "cl-core-opt": generate_options(test["core"]),
1130                         "cl-core-val": [last_test["core"].upper(), ],
1131                         "cl-core-all-val": list(),
1132                         "cl-core-all-opt": C.CL_ALL_ENABLED,
1133                         "cl-frmsize-opt": generate_options(test["frame-size"]),
1134                         "cl-frmsize-val": [last_test["framesize"].upper(), ],
1135                         "cl-frmsize-all-val": list(),
1136                         "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1137                         "cl-tsttype-opt": generate_options(test["test-type"]),
1138                         "cl-tsttype-val": [last_test["testtype"].upper(), ],
1139                         "cl-tsttype-all-val": list(),
1140                         "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1141                         "cl-normalize-val": normalize,
1142                         "btn-add-dis": False
1143                     })
1144                     on_draw = True
1145             elif trigger.type == "normalize":
1146                 ctrl_panel.set({"cl-normalize-val": normalize})
1147                 on_draw = True
1148             elif trigger.type == "ctrl-dd":
1149                 if trigger.idx == "dut":
1150                     try:
1151                         options = generate_options(
1152                             self._spec_tbs[trigger.value].keys()
1153                         )
1154                         disabled = False
1155                     except KeyError:
1156                         options = list()
1157                         disabled = True
1158                     ctrl_panel.set({
1159                         "dd-dut-val": trigger.value,
1160                         "dd-phy-val": str(),
1161                         "dd-phy-opt": options,
1162                         "dd-phy-dis": disabled,
1163                         "dd-area-val": str(),
1164                         "dd-area-opt": list(),
1165                         "dd-area-dis": True,
1166                         "dd-test-val": str(),
1167                         "dd-test-opt": list(),
1168                         "dd-test-dis": True,
1169                         "cl-core-opt": list(),
1170                         "cl-core-val": list(),
1171                         "cl-core-all-val": list(),
1172                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1173                         "cl-frmsize-opt": list(),
1174                         "cl-frmsize-val": list(),
1175                         "cl-frmsize-all-val": list(),
1176                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1177                         "cl-tsttype-opt": list(),
1178                         "cl-tsttype-val": list(),
1179                         "cl-tsttype-all-val": list(),
1180                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1181                         "btn-add-dis": True
1182                     })
1183                 elif trigger.idx == "phy":
1184                     try:
1185                         dut = ctrl_panel.get("dd-dut-val")
1186                         phy = self._spec_tbs[dut][trigger.value]
1187                         options = [{"label": label(v), "value": v} \
1188                             for v in sorted(phy.keys())]
1189                         disabled = False
1190                     except KeyError:
1191                         options = list()
1192                         disabled = True
1193                     ctrl_panel.set({
1194                         "dd-phy-val": trigger.value,
1195                         "dd-area-val": str(),
1196                         "dd-area-opt": options,
1197                         "dd-area-dis": disabled,
1198                         "dd-test-val": str(),
1199                         "dd-test-opt": list(),
1200                         "dd-test-dis": True,
1201                         "cl-core-opt": list(),
1202                         "cl-core-val": list(),
1203                         "cl-core-all-val": list(),
1204                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1205                         "cl-frmsize-opt": list(),
1206                         "cl-frmsize-val": list(),
1207                         "cl-frmsize-all-val": list(),
1208                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1209                         "cl-tsttype-opt": list(),
1210                         "cl-tsttype-val": list(),
1211                         "cl-tsttype-all-val": list(),
1212                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1213                         "btn-add-dis": True
1214                     })  
1215                 elif trigger.idx == "area":
1216                     try:
1217                         dut = ctrl_panel.get("dd-dut-val")
1218                         phy = ctrl_panel.get("dd-phy-val")
1219                         area = self._spec_tbs[dut][phy][trigger.value]
1220                         options = generate_options(area.keys())
1221                         disabled = False
1222                     except KeyError:
1223                         options = list()
1224                         disabled = True
1225                     ctrl_panel.set({
1226                         "dd-area-val": trigger.value,
1227                         "dd-test-val": str(),
1228                         "dd-test-opt": options,
1229                         "dd-test-dis": disabled,
1230                         "cl-core-opt": list(),
1231                         "cl-core-val": list(),
1232                         "cl-core-all-val": list(),
1233                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1234                         "cl-frmsize-opt": list(),
1235                         "cl-frmsize-val": list(),
1236                         "cl-frmsize-all-val": list(),
1237                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1238                         "cl-tsttype-opt": list(),
1239                         "cl-tsttype-val": list(),
1240                         "cl-tsttype-all-val": list(),
1241                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1242                         "btn-add-dis": True
1243                     })
1244                 elif trigger.idx == "test":
1245                     dut = ctrl_panel.get("dd-dut-val")
1246                     phy = ctrl_panel.get("dd-phy-val")
1247                     area = ctrl_panel.get("dd-area-val")
1248                     if all((dut, phy, area, trigger.value, )):
1249                         test = self._spec_tbs[dut][phy][area][trigger.value]
1250                         ctrl_panel.set({
1251                             "dd-test-val": trigger.value,
1252                             "cl-core-opt": generate_options(test["core"]),
1253                             "cl-core-val": list(),
1254                             "cl-core-all-val": list(),
1255                             "cl-core-all-opt": C.CL_ALL_ENABLED,
1256                             "cl-frmsize-opt": \
1257                                 generate_options(test["frame-size"]),
1258                             "cl-frmsize-val": list(),
1259                             "cl-frmsize-all-val": list(),
1260                             "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1261                             "cl-tsttype-opt": \
1262                                 generate_options(test["test-type"]),
1263                             "cl-tsttype-val": list(),
1264                             "cl-tsttype-all-val": list(),
1265                             "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1266                             "btn-add-dis": True
1267                         })
1268             elif trigger.type == "ctrl-cl":
1269                 param = trigger.idx.split("-")[0]
1270                 if "-all" in trigger.idx:
1271                     c_sel, c_all, c_id = list(), trigger.value, "all"
1272                 else:
1273                     c_sel, c_all, c_id = trigger.value, list(), str()
1274                 val_sel, val_all = sync_checklists(
1275                     options=ctrl_panel.get(f"cl-{param}-opt"),
1276                     sel=c_sel,
1277                     all=c_all,
1278                     id=c_id
1279                 )
1280                 ctrl_panel.set({
1281                     f"cl-{param}-val": val_sel,
1282                     f"cl-{param}-all-val": val_all,
1283                 })
1284                 if all((ctrl_panel.get("cl-core-val"), 
1285                         ctrl_panel.get("cl-frmsize-val"),
1286                         ctrl_panel.get("cl-tsttype-val"), )):
1287                     ctrl_panel.set({"btn-add-dis": False})
1288                 else:
1289                     ctrl_panel.set({"btn-add-dis": True})
1290             elif trigger.type == "ctrl-btn":
1291                 on_draw = True
1292                 if trigger.idx == "add-test":
1293                     dut = ctrl_panel.get("dd-dut-val")
1294                     phy = ctrl_panel.get("dd-phy-val")
1295                     area = ctrl_panel.get("dd-area-val")
1296                     test = ctrl_panel.get("dd-test-val")
1297                     # Add selected test(s) to the list of tests in store:
1298                     if store_sel is None:
1299                         store_sel = list()
1300                     for core in ctrl_panel.get("cl-core-val"):
1301                         for framesize in ctrl_panel.get("cl-frmsize-val"):
1302                             for ttype in ctrl_panel.get("cl-tsttype-val"):
1303                                 if dut == "trex":
1304                                     core = str()
1305                                 tid = "-".join((
1306                                     dut,
1307                                     phy.replace('af_xdp', 'af-xdp'),
1308                                     area,
1309                                     framesize.lower(),
1310                                     core.lower(),
1311                                     test,
1312                                     ttype.lower()
1313                                 ))
1314                                 if tid not in [i["id"] for i in store_sel]:
1315                                     store_sel.append({
1316                                         "id": tid,
1317                                         "dut": dut,
1318                                         "phy": phy,
1319                                         "area": area,
1320                                         "test": test,
1321                                         "framesize": framesize.lower(),
1322                                         "core": core.lower(),
1323                                         "testtype": ttype.lower()
1324                                     })
1325                     store_sel = sorted(store_sel, key=lambda d: d["id"])
1326                     if C.CLEAR_ALL_INPUTS:
1327                         ctrl_panel.set(ctrl_panel.defaults)
1328                 elif trigger.idx == "rm-test" and lst_sel:
1329                     new_store_sel = list()
1330                     for idx, item in enumerate(store_sel):
1331                         if not lst_sel[idx]:
1332                             new_store_sel.append(item)
1333                     store_sel = new_store_sel
1334                 elif trigger.idx == "rm-test-all":
1335                     store_sel = list()
1336
1337             if on_draw:
1338                 if store_sel:
1339                     lg_selected = get_list_group_items(store_sel, "sel-cl")
1340                     plotting_area_trending = self._get_plotting_area_trending(
1341                         store_sel,
1342                         bool(normalize),
1343                         gen_new_url(
1344                             parsed_url,
1345                             {"store_sel": store_sel, "norm": normalize}
1346                         )
1347                     )
1348                     plotting_area_buttons = self._get_plotting_area_buttons()
1349                     row_card_sel_tests = C.STYLE_ENABLED
1350                     row_btns_sel_tests = C.STYLE_ENABLED
1351                 else:
1352                     plotting_area_trending = C.PLACEHOLDER
1353                     plotting_area_buttons = C.PLACEHOLDER
1354                     row_card_sel_tests = C.STYLE_DISABLED
1355                     row_btns_sel_tests = C.STYLE_DISABLED
1356                     lg_selected = no_update
1357                     store_sel = list()
1358             else:
1359                 plotting_area_trending = no_update
1360                 plotting_area_buttons = no_update
1361                 row_card_sel_tests = no_update
1362                 row_btns_sel_tests = no_update
1363                 lg_selected = no_update
1364
1365             ret_val = [
1366                 ctrl_panel.panel,
1367                 store_sel,
1368                 plotting_area_trending,
1369                 plotting_area_buttons,
1370                 row_card_sel_tests,
1371                 row_btns_sel_tests,
1372                 lg_selected
1373             ]
1374             ret_val.extend(ctrl_panel.values)
1375             return ret_val
1376
1377         @app.callback(
1378             Output("plot-mod-url", "is_open"),
1379             Input("plot-btn-url", "n_clicks"),
1380             State("plot-mod-url", "is_open")
1381         )
1382         def toggle_plot_mod_url(n, is_open):
1383             """Toggle the modal window with url.
1384             """
1385             if n:
1386                 return not is_open
1387             return is_open
1388
1389         @app.callback(
1390             Output("store-telemetry-data", "data"),
1391             Output("store-telemetry-user", "data"),
1392             Output("telemetry-search-in", "value"),
1393             Output("telemetry-search-out", "children"),
1394             Output("telemetry-list-metrics", "value"),
1395             Output("telemetry-dd", "children"),
1396             Output("plotting-area-telemetry", "children"),
1397             Output("plot-mod-telemetry-1", "is_open"),
1398             Output("plot-mod-telemetry-2", "is_open"),
1399             Output({"type": "telemetry-btn", "index": "select"}, "disabled"),
1400             Output({"type": "telemetry-btn", "index": "add"}, "disabled"),
1401             State("store-telemetry-data", "data"),
1402             State("store-telemetry-user", "data"),
1403             State("store-selected-tests", "data"),
1404             Input({"type": "tele-cl", "index": ALL}, "value"),
1405             Input("telemetry-search-in", "value"),
1406             Input({"type": "telemetry-btn", "index": ALL}, "n_clicks"),
1407             Input({"type": "tm-dd", "index": ALL}, "value"),
1408             prevent_initial_call=True
1409         )
1410         def _update_plot_mod_telemetry(
1411                 tm_data: dict,
1412                 tm_user: dict,
1413                 store_sel: list,
1414                 cl_metrics: list,
1415                 search_in: str,
1416                 n_clicks: list,
1417                 tm_dd_in: list
1418             ) -> tuple:
1419             """Toggle the modal window with telemetry.
1420             """
1421
1422             if not any(n_clicks):
1423                 raise PreventUpdate
1424
1425             if tm_user is None:
1426                 # Telemetry user data
1427                 # The data provided by user or result of user action
1428                 tm_user = {
1429                     # List of unique metrics:
1430                     "unique_metrics": list(),
1431                     # List of metrics selected by user:
1432                     "selected_metrics": list(),
1433                     # Labels from metrics selected by user (key: label name,
1434                     # value: list of all possible values):
1435                     "unique_labels": dict(),
1436                     # Labels selected by the user (subset of 'unique_labels'):
1437                     "selected_labels": dict(),
1438                     # All unique metrics with labels (output from the step 1)
1439                     # converted from pandas dataframe to dictionary.
1440                     "unique_metrics_with_labels": dict(),
1441                     # Metrics with labels selected by the user using dropdowns.
1442                     "selected_metrics_with_labels": dict()
1443                 }
1444
1445             tm = TelemetryData(tests=store_sel)
1446             tm_json = no_update
1447             search_out = no_update
1448             list_metrics = no_update
1449             tm_dd = no_update
1450             plotting_area_telemetry = no_update
1451             is_open = (False, False)
1452             is_btn_disabled = (True, True)
1453
1454             trigger = Trigger(callback_context.triggered)
1455             if trigger.type == "telemetry-btn":
1456                 if trigger.idx in ("open", "back"):
1457                     tm.from_dataframe(self._data)
1458                     tm_json = tm.to_json()
1459                     tm_user["unique_metrics"] = tm.unique_metrics
1460                     tm_user["selected_metrics"] = list()
1461                     tm_user["unique_labels"] = dict()
1462                     tm_user["selected_labels"] = dict()
1463                     search_in = str()
1464                     search_out = get_list_group_items(
1465                         tm_user["unique_metrics"],
1466                         "tele-cl",
1467                         False
1468                     )
1469                     is_open = (True, False)
1470                 elif trigger.idx == "select":
1471                     tm.from_json(tm_data)
1472                     if any(cl_metrics):
1473                         if not tm_user["selected_metrics"]:
1474                             tm_user["selected_metrics"] = \
1475                                 tm_user["unique_metrics"]
1476                         metrics = [a for a, b in \
1477                             zip(tm_user["selected_metrics"], cl_metrics) if b]
1478                         tm_user["selected_metrics"] = metrics
1479                         tm_user["unique_labels"] = \
1480                             tm.get_selected_labels(metrics)
1481                         tm_user["unique_metrics_with_labels"] = \
1482                             tm.unique_metrics_with_labels
1483                         list_metrics = tm.str_metrics
1484                         tm_dd = _get_dd_container(tm_user["unique_labels"])
1485                         if list_metrics:
1486                             is_btn_disabled = (True, False)
1487                         is_open = (False, True)
1488                     else:
1489                         tm_user = None
1490                         is_open = (False, False)
1491                 elif trigger.idx == "add":
1492                     tm.from_json(tm_data)
1493                     plotting_area_telemetry = self._get_plotting_area_telemetry(
1494                         graph_tm_trending(
1495                             tm.select_tm_trending_data(
1496                                 tm_user["selected_metrics_with_labels"]
1497                             ),
1498                             self._graph_layout)
1499                     )
1500                     tm_user = None
1501                     is_open = (False, False)
1502                 elif trigger.idx == "cancel":
1503                     tm_user = None
1504                     is_open = (False, False)
1505             elif trigger.type == "telemetry-search-in":
1506                 tm.from_metrics(tm_user["unique_metrics"])
1507                 tm_user["selected_metrics"] = \
1508                     tm.search_unique_metrics(search_in)
1509                 search_out = get_list_group_items(
1510                     tm_user["selected_metrics"],
1511                     type="tele-cl",
1512                     colorize=False
1513                 )
1514                 is_open = (True, False)
1515             elif trigger.type == "tele-cl":
1516                 if any(cl_metrics):
1517                     is_btn_disabled = (False, True)
1518                 is_open = (True, False)
1519             elif trigger.type == "tm-dd":
1520                 tm.from_metrics_with_labels(
1521                     tm_user["unique_metrics_with_labels"]
1522                 )
1523                 selected = dict()
1524                 previous_itm = None
1525                 for itm in tm_dd_in:
1526                     if itm is None:
1527                         show_new = True
1528                     elif isinstance(itm, str):
1529                         show_new = False
1530                         selected[itm] = list()
1531                     elif isinstance(itm, list):
1532                         if previous_itm is not None:
1533                             selected[previous_itm] = itm
1534                         show_new = True
1535                     previous_itm = itm
1536
1537                 tm_dd = _get_dd_container(
1538                     tm_user["unique_labels"],
1539                     selected,
1540                     show_new
1541                 )
1542                 sel_metrics = tm.filter_selected_metrics_by_labels(selected)
1543                 tm_user["selected_metrics_with_labels"] = sel_metrics.to_dict()
1544                 if not sel_metrics.empty:
1545                     list_metrics = tm.metrics_to_str(sel_metrics)
1546                 else:
1547                     list_metrics = str()
1548                 if list_metrics:
1549                     is_btn_disabled = (True, False)
1550                 is_open = (False, True)
1551
1552             # Return values:
1553             ret_val = [
1554                 tm_json,
1555                 tm_user,
1556                 search_in,
1557                 search_out,
1558                 list_metrics,
1559                 tm_dd,
1560                 plotting_area_telemetry
1561             ]
1562             ret_val.extend(is_open)
1563             ret_val.extend(is_btn_disabled)
1564             return ret_val
1565
1566         def _get_dd_container(
1567                 all_labels: dict,
1568                 selected_labels: dict=dict(),
1569                 show_new=True
1570             ) -> list:
1571             """Generate a container with dropdown selection boxes depenting on
1572             the input data.
1573
1574             :param all_labels: A dictionary with unique labels and their
1575                 possible values.
1576             :param selected_labels: A dictionalry with user selected lables and
1577                 their values.
1578             :param show_new: If True, a dropdown selection box to add a new
1579                 label is displayed.
1580             :type all_labels: dict
1581             :type selected_labels: dict
1582             :type show_new: bool
1583             :returns: A list of dbc rows with dropdown selection boxes.
1584             :rtype: list
1585             """
1586
1587             def _row(
1588                     id: str,
1589                     lopts: list=list(),
1590                     lval: str=str(),
1591                     vopts: list=list(),
1592                     vvals: list=list()
1593                 ) -> dbc.Row:
1594                 """Generates a dbc row with dropdown boxes.
1595
1596                 :param id: A string added to the dropdown ID.
1597                 :param lopts: A list of options for 'label' dropdown.
1598                 :param lval: Value of 'label' dropdown.
1599                 :param vopts: A list of options for 'value' dropdown.
1600                 :param vvals: A list of values for 'value' dropdown.
1601                 :type id: str
1602                 :type lopts: list
1603                 :type lval: str
1604                 :type vopts: list
1605                 :type vvals: list
1606                 :returns: dbc row with dropdown boxes.
1607                 :rtype: dbc.Row
1608                 """
1609                 children = list()
1610                 if lopts:
1611                     children.append(
1612                         dbc.Col(
1613                             width=6,
1614                             children=[
1615                                 dcc.Dropdown(
1616                                     id={
1617                                         "type": "tm-dd",
1618                                         "index": f"label-{id}"
1619                                     },
1620                                     placeholder="Select a label...",
1621                                     optionHeight=20,
1622                                     multi=False,
1623                                     options=lopts,
1624                                     value=lval if lval else None
1625                                 )
1626                             ]
1627                         )
1628                     )
1629                     if vopts:
1630                         children.append(
1631                             dbc.Col(
1632                                 width=6,
1633                                 children=[
1634                                     dcc.Dropdown(
1635                                         id={
1636                                             "type": "tm-dd",
1637                                             "index": f"value-{id}"
1638                                         },
1639                                         placeholder="Select a value...",
1640                                         optionHeight=20,
1641                                         multi=True,
1642                                         options=vopts,
1643                                         value=vvals if vvals else None
1644                                     )
1645                                 ]
1646                             )
1647                         )
1648
1649                 return dbc.Row(class_name="g-0 p-1", children=children)
1650
1651             container = list()
1652
1653             # Display rows with items in 'selected_labels'; label on the left,
1654             # values on the right:
1655             keys_left = list(all_labels.keys())
1656             for idx, label in enumerate(selected_labels.keys()):
1657                 container.append(_row(
1658                     id=idx,
1659                     lopts=deepcopy(keys_left),
1660                     lval=label,
1661                     vopts=all_labels[label],
1662                     vvals=selected_labels[label]
1663                 ))
1664                 keys_left.remove(label)
1665
1666             # Display row with dd with labels on the left, right side is empty:
1667             if show_new and keys_left:
1668                 container.append(_row(id="new", lopts=keys_left))
1669
1670             return container
1671
1672         @app.callback(
1673             Output("metadata-tput-lat", "children"),
1674             Output("metadata-hdrh-graph", "children"),
1675             Output("offcanvas-metadata", "is_open"),
1676             Input({"type": "graph", "index": ALL}, "clickData"),
1677             prevent_initial_call=True
1678         )
1679         def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1680             """Generates the data for the offcanvas displayed when a particular
1681             point in a graph is clicked on.
1682
1683             :param graph_data: The data from the clicked point in the graph.
1684             :type graph_data: dict
1685             :returns: The data to be displayed on the offcanvas and the
1686                 information to show the offcanvas.
1687             :rtype: tuple(list, list, bool)
1688             """
1689
1690             trigger = Trigger(callback_context.triggered)
1691
1692             try:
1693                 idx = 0 if trigger.idx == "tput" else 1
1694                 graph_data = graph_data[idx]["points"][0]
1695             except (IndexError, KeyError, ValueError, TypeError):
1696                 raise PreventUpdate
1697
1698             metadata = no_update
1699             graph = list()
1700
1701             children = [
1702                 dbc.ListGroupItem(
1703                     [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1704                 ) for x in graph_data.get("text", "").split("<br>")
1705             ]
1706             if trigger.idx == "tput":
1707                 title = "Throughput"
1708             elif trigger.idx == "lat":
1709                 title = "Latency"
1710                 hdrh_data = graph_data.get("customdata", None)
1711                 if hdrh_data:
1712                     graph = [dbc.Card(
1713                         class_name="gy-2 p-0",
1714                         children=[
1715                             dbc.CardHeader(hdrh_data.pop("name")),
1716                             dbc.CardBody(children=[
1717                                 dcc.Graph(
1718                                     id="hdrh-latency-graph",
1719                                     figure=graph_hdrh_latency(
1720                                         hdrh_data, self._graph_layout
1721                                     )
1722                                 )
1723                             ])
1724                         ])
1725                     ]
1726             else:
1727                 raise PreventUpdate
1728
1729             metadata = [
1730                 dbc.Card(
1731                     class_name="gy-2 p-0",
1732                     children=[
1733                         dbc.CardHeader(children=[
1734                             dcc.Clipboard(
1735                                 target_id="tput-lat-metadata",
1736                                 title="Copy",
1737                                 style={"display": "inline-block"}
1738                             ),
1739                             title
1740                         ]),
1741                         dbc.CardBody(
1742                             id="tput-lat-metadata",
1743                             class_name="p-0",
1744                             children=[dbc.ListGroup(children, flush=True), ]
1745                         )
1746                     ]
1747                 )
1748             ]
1749
1750             return metadata, graph, True
1751
1752         @app.callback(
1753             Output("download-trending-data", "data"),
1754             State("store-selected-tests", "data"),
1755             Input("plot-btn-download", "n_clicks"),
1756             prevent_initial_call=True
1757         )
1758         def _download_trending_data(store_sel: list, _) -> dict:
1759             """Download the data
1760
1761             :param store_sel: List of tests selected by user stored in the
1762                 browser.
1763             :type store_sel: list
1764             :returns: dict of data frame content (base64 encoded) and meta data
1765                 used by the Download component.
1766             :rtype: dict
1767             """
1768
1769             if not store_sel:
1770                 raise PreventUpdate
1771
1772             df = pd.DataFrame()
1773             for itm in store_sel:
1774                 sel_data = select_trending_data(self._data, itm)
1775                 if sel_data is None:
1776                     continue
1777                 df = pd.concat([df, sel_data], ignore_index=True, copy=False)
1778
1779             return dcc.send_data_frame(df.to_csv, C.TREND_DOWNLOAD_FILE_NAME)