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