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