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