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