C-Dash: Start telemetry accordions collapsed
[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                                 start_collapsed=True,
889                                 always_open=True,
890                                 flush=True
891                             ),
892                             class_name="g-0 p-0"
893                         ),
894                         dbc.Row(
895                             html.Div(
896                                 [
897                                     dbc.Button(
898                                         "Remove",
899                                         id={
900                                             "type": "tm-btn-remove",
901                                             "index": idx
902                                         },
903                                         class_name="me-1",
904                                         color="danger",
905                                         style={"padding": "0rem 1rem"}
906                                     ),
907                                     dbc.Button(
908                                         "Download Data",
909                                         id={
910                                             "type": "tm-btn-download",
911                                             "index": idx
912                                         },
913                                         class_name="me-1",
914                                         color="info",
915                                         style={"padding": "0rem 1rem"}
916                                     )
917                                 ],
918                             className=\
919                                 "d-grid gap-0 d-md-flex justify-content-md-end"
920                             ),
921                             class_name="g-0 p-0"
922                         )
923                     ],
924                     class_name="g-0 p-0",
925                     title=(
926                         f"Metric{_plural(graph_set[1])}: ",
927                         ", ".join(graph_set[1])
928                     )
929                 )
930             )
931
932         return dbc.Col(
933             dbc.Accordion(
934                 panels,
935                 class_name="g-0 p-1",
936                 start_collapsed=True,
937                 always_open=True
938             )
939         )
940
941     @staticmethod
942     def _get_telemetry_step_1() -> list:
943         """Return the content of the modal window used in the step 1 of metrics
944         selection.
945
946         :returns: A list of dbc rows with 'input' and 'search output'.
947         :rtype: list
948         """
949         return [
950             dbc.Row(
951                 class_name="g-0 p-1",
952                 children=[
953                     dbc.Input(
954                         id={"type": "telemetry-search-in", "index": 0},
955                         placeholder="Start typing a metric name...",
956                         type="text"
957                     )
958                 ]
959             ),
960             dbc.Row(
961                 class_name="g-0 p-1",
962                 children=[
963                     dbc.ListGroup(
964                         class_name="overflow-auto p-0",
965                         id={"type": "telemetry-search-out", "index": 0},
966                         children=[],
967                         style={"max-height": "14em"},
968                         flush=True
969                     )
970                 ]
971             )
972         ]
973
974     @staticmethod
975     def _get_telemetry_step_2() -> list:
976         """Return the content of the modal window used in the step 2 of metrics
977         selection.
978
979         :returns: A list of dbc rows with 'container with dynamic dropdowns' and
980             'search output'.
981         :rtype: list
982         """
983         return [
984             dbc.Row(
985                 id={"type": "tm-container", "index": 0},
986                 class_name="g-0 p-1",
987                 children=["Add content here."]
988             ),
989             dbc.Row(
990                 class_name="g-0 p-2",
991                 children=[
992                     dbc.Checkbox(
993                         id={"type": "cb-all-in-one", "index": 0},
994                         label="All Metrics in one Graph"
995                     ),
996                 ]
997             ),
998             dbc.Row(
999                 class_name="g-0 p-1",
1000                 children=[
1001                     dbc.Textarea(
1002                         id={"type": "tm-list-metrics", "index": 0},
1003                         rows=20,
1004                         size="sm",
1005                         wrap="off",
1006                         readonly=True
1007                     )
1008                 ]
1009             )
1010         ]
1011
1012     def callbacks(self, app):
1013         """Callbacks for the whole application.
1014
1015         :param app: The application.
1016         :type app: Flask
1017         """
1018
1019         @app.callback(
1020             Output("store", "data"),
1021             Output("plotting-area-trending", "children"),
1022             Output("plotting-area-telemetry", "children"),
1023             Output("col-plotting-area", "style"),
1024             Output("row-card-sel-tests", "style"),
1025             Output("row-btns-sel-tests", "style"),
1026             Output("row-btns-add-tm", "style"),
1027             Output("lg-selected", "children"),
1028             Output({"type": "telemetry-search-out", "index": ALL}, "children"),
1029             Output({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
1030             Output({"type": "telemetry-btn", "index": ALL}, "disabled"),
1031             Output({"type": "tm-container", "index": ALL}, "children"),
1032             Output({"type": "tm-list-metrics", "index": ALL}, "value"),
1033             Output({"type": "ctrl-dd", "index": "dut"}, "value"),
1034             Output({"type": "ctrl-dd", "index": "phy"}, "options"),
1035             Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
1036             Output({"type": "ctrl-dd", "index": "phy"}, "value"),
1037             Output({"type": "ctrl-dd", "index": "area"}, "options"),
1038             Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
1039             Output({"type": "ctrl-dd", "index": "area"}, "value"),
1040             Output({"type": "ctrl-dd", "index": "test"}, "options"),
1041             Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
1042             Output({"type": "ctrl-dd", "index": "test"}, "value"),
1043             Output({"type": "ctrl-cl", "index": "core"}, "options"),
1044             Output({"type": "ctrl-cl", "index": "core"}, "value"),
1045             Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
1046             Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
1047             Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
1048             Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
1049             Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
1050             Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
1051             Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
1052             Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
1053             Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
1054             Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
1055             Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
1056             Output("normalize", "value"),
1057
1058             State("store", "data"),
1059             State({"type": "sel-cl", "index": ALL}, "value"),
1060             State({"type": "cb-all-in-one", "index": ALL}, "value"),
1061             State({"type": "telemetry-search-out", "index": ALL}, "children"),
1062             State({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
1063             State({"type": "telemetry-btn", "index": ALL}, "disabled"),
1064             State({"type": "tm-container", "index": ALL}, "children"),
1065             State({"type": "tm-list-metrics", "index": ALL}, "value"),
1066
1067             Input("url", "href"),
1068             Input({"type": "tele-cl", "index": ALL}, "value"),
1069             Input({"type": "tm-dd", "index": ALL}, "value"),
1070
1071             Input("normalize", "value"),
1072             Input({"type": "telemetry-search-in", "index": ALL}, "value"),
1073             Input({"type": "telemetry-btn", "index": ALL}, "n_clicks"),
1074             Input({"type": "tm-btn-remove", "index": ALL}, "n_clicks"),
1075             Input({"type": "ctrl-dd", "index": ALL}, "value"),
1076             Input({"type": "ctrl-cl", "index": ALL}, "value"),
1077             Input({"type": "ctrl-btn", "index": ALL}, "n_clicks"),
1078
1079             prevent_initial_call=True
1080         )
1081         def _update_application(
1082                 store: dict,
1083                 lst_sel: list,
1084                 all_in_one: list,
1085                 search_out: list,
1086                 is_open: list,
1087                 tm_btns_disabled: list,
1088                 tm_dd: list,
1089                 list_metrics: list,
1090                 href: str,
1091                 cl_metrics: list,
1092                 tm_dd_in: list,
1093                 *_
1094             ) -> tuple:
1095             """Update the application when the event is detected.
1096             """
1097
1098             if store is None:
1099                 store = {
1100                     "control-panel": dict(),
1101                     "selected-tests": list(),
1102                     "telemetry-data": dict(),
1103                     "selected-metrics": dict(),
1104                     "telemetry-panels": list(),
1105                     "telemetry-all-in-one": list(),
1106                     "url": str()
1107                 }
1108
1109             ctrl_panel = ControlPanel(
1110                 CP_PARAMS,
1111                 store.get("control-panel", dict())
1112             )
1113             store_sel = store["selected-tests"]
1114             tm_data = store["telemetry-data"]
1115             tm_user = store["selected-metrics"]
1116             tm_panels = store["telemetry-panels"]
1117             tm_all_in_one = store["telemetry-all-in-one"]
1118
1119             plotting_area_telemetry = no_update
1120             on_draw = [False, False]  # 0 --> trending, 1 --> telemetry
1121
1122             # Parse the url:
1123             parsed_url = url_decode(href)
1124             if parsed_url:
1125                 url_params = parsed_url["params"]
1126             else:
1127                 url_params = None
1128
1129             if tm_user is None:
1130                 # Telemetry user data
1131                 # The data provided by user or result of user action
1132                 tm_user = {
1133                     # List of unique metrics:
1134                     "unique_metrics": list(),
1135                     # List of metrics selected by user:
1136                     "selected_metrics": list(),
1137                     # Labels from metrics selected by user (key: label name,
1138                     # value: list of all possible values):
1139                     "unique_labels": dict(),
1140                     # Labels selected by the user (subset of 'unique_labels'):
1141                     "selected_labels": dict(),
1142                     # All unique metrics with labels (output from the step 1)
1143                     # converted from pandas dataframe to dictionary.
1144                     "unique_metrics_with_labels": dict(),
1145                     # Metrics with labels selected by the user using dropdowns.
1146                     "selected_metrics_with_labels": dict()
1147                 }
1148             tm = TelemetryData(store_sel) if store_sel else TelemetryData()
1149
1150             trigger = Trigger(callback_context.triggered)
1151             if trigger.type == "url" and url_params:
1152                 telemetry = None
1153                 try:
1154                     store_sel = literal_eval(url_params["store_sel"][0])
1155                     normalize = literal_eval(url_params["norm"][0])
1156                     telemetry = literal_eval(url_params["telemetry"][0])
1157                     tm_all_in_one = literal_eval(url_params["all-in-one"][0])
1158                     if not isinstance(telemetry, list):
1159                         telemetry = [telemetry, ]
1160                         tm_all_in_one = [tm_all_in_one, ]
1161                 except (KeyError, IndexError, AttributeError, ValueError):
1162                     pass
1163                 if store_sel:
1164                     last_test = store_sel[-1]
1165                     test = self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1166                         [last_test["area"]][last_test["test"]]
1167                     ctrl_panel.set({
1168                         "dd-dut-val": last_test["dut"],
1169                         "dd-phy-val": last_test["phy"],
1170                         "dd-phy-opt": generate_options(
1171                             self._spec_tbs[last_test["dut"]].keys()
1172                         ),
1173                         "dd-phy-dis": False,
1174                         "dd-area-val": last_test["area"],
1175                         "dd-area-opt": [
1176                             {"label": label(v), "value": v} for v in sorted(
1177                                 self._spec_tbs[last_test["dut"]]\
1178                                     [last_test["phy"]].keys()
1179                             )
1180                         ],
1181                         "dd-area-dis": False,
1182                         "dd-test-val": last_test["test"],
1183                         "dd-test-opt": generate_options(
1184                             self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1185                                 [last_test["area"]].keys()
1186                         ),
1187                         "dd-test-dis": False,
1188                         "cl-core-opt": generate_options(test["core"]),
1189                         "cl-core-val": [last_test["core"].upper(), ],
1190                         "cl-core-all-val": list(),
1191                         "cl-core-all-opt": C.CL_ALL_ENABLED,
1192                         "cl-frmsize-opt": generate_options(test["frame-size"]),
1193                         "cl-frmsize-val": [last_test["framesize"].upper(), ],
1194                         "cl-frmsize-all-val": list(),
1195                         "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1196                         "cl-tsttype-opt": generate_options(test["test-type"]),
1197                         "cl-tsttype-val": [last_test["testtype"].upper(), ],
1198                         "cl-tsttype-all-val": list(),
1199                         "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1200                         "cl-normalize-val": normalize,
1201                         "btn-add-dis": False
1202                     })
1203                     on_draw[0] = True
1204                     if telemetry:
1205                         tm = TelemetryData(store_sel)
1206                         tm.from_dataframe(self._data)
1207                         tm_data = tm.to_json()
1208                         tm.from_json(tm_data)
1209                         tm_panels = telemetry
1210                         on_draw[1] = True
1211             elif trigger.type == "normalize":
1212                 ctrl_panel.set({"cl-normalize-val": trigger.value})
1213                 on_draw[0] = True
1214             elif trigger.type == "ctrl-dd":
1215                 if trigger.idx == "dut":
1216                     try:
1217                         options = generate_options(
1218                             self._spec_tbs[trigger.value].keys()
1219                         )
1220                         disabled = False
1221                     except KeyError:
1222                         options = list()
1223                         disabled = True
1224                     ctrl_panel.set({
1225                         "dd-dut-val": trigger.value,
1226                         "dd-phy-val": str(),
1227                         "dd-phy-opt": options,
1228                         "dd-phy-dis": disabled,
1229                         "dd-area-val": str(),
1230                         "dd-area-opt": list(),
1231                         "dd-area-dis": True,
1232                         "dd-test-val": str(),
1233                         "dd-test-opt": list(),
1234                         "dd-test-dis": True,
1235                         "cl-core-opt": list(),
1236                         "cl-core-val": list(),
1237                         "cl-core-all-val": list(),
1238                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1239                         "cl-frmsize-opt": list(),
1240                         "cl-frmsize-val": list(),
1241                         "cl-frmsize-all-val": list(),
1242                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1243                         "cl-tsttype-opt": list(),
1244                         "cl-tsttype-val": list(),
1245                         "cl-tsttype-all-val": list(),
1246                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1247                         "btn-add-dis": True
1248                     })
1249                 elif trigger.idx == "phy":
1250                     try:
1251                         dut = ctrl_panel.get("dd-dut-val")
1252                         phy = self._spec_tbs[dut][trigger.value]
1253                         options = [{"label": label(v), "value": v} \
1254                             for v in sorted(phy.keys())]
1255                         disabled = False
1256                     except KeyError:
1257                         options = list()
1258                         disabled = True
1259                     ctrl_panel.set({
1260                         "dd-phy-val": trigger.value,
1261                         "dd-area-val": str(),
1262                         "dd-area-opt": options,
1263                         "dd-area-dis": disabled,
1264                         "dd-test-val": str(),
1265                         "dd-test-opt": list(),
1266                         "dd-test-dis": True,
1267                         "cl-core-opt": list(),
1268                         "cl-core-val": list(),
1269                         "cl-core-all-val": list(),
1270                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1271                         "cl-frmsize-opt": list(),
1272                         "cl-frmsize-val": list(),
1273                         "cl-frmsize-all-val": list(),
1274                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1275                         "cl-tsttype-opt": list(),
1276                         "cl-tsttype-val": list(),
1277                         "cl-tsttype-all-val": list(),
1278                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1279                         "btn-add-dis": True
1280                     })  
1281                 elif trigger.idx == "area":
1282                     try:
1283                         dut = ctrl_panel.get("dd-dut-val")
1284                         phy = ctrl_panel.get("dd-phy-val")
1285                         area = self._spec_tbs[dut][phy][trigger.value]
1286                         options = generate_options(area.keys())
1287                         disabled = False
1288                     except KeyError:
1289                         options = list()
1290                         disabled = True
1291                     ctrl_panel.set({
1292                         "dd-area-val": trigger.value,
1293                         "dd-test-val": str(),
1294                         "dd-test-opt": options,
1295                         "dd-test-dis": disabled,
1296                         "cl-core-opt": list(),
1297                         "cl-core-val": list(),
1298                         "cl-core-all-val": list(),
1299                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1300                         "cl-frmsize-opt": list(),
1301                         "cl-frmsize-val": list(),
1302                         "cl-frmsize-all-val": list(),
1303                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1304                         "cl-tsttype-opt": list(),
1305                         "cl-tsttype-val": list(),
1306                         "cl-tsttype-all-val": list(),
1307                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1308                         "btn-add-dis": True
1309                     })
1310                 elif trigger.idx == "test":
1311                     dut = ctrl_panel.get("dd-dut-val")
1312                     phy = ctrl_panel.get("dd-phy-val")
1313                     area = ctrl_panel.get("dd-area-val")
1314                     if all((dut, phy, area, trigger.value, )):
1315                         test = self._spec_tbs[dut][phy][area][trigger.value]
1316                         ctrl_panel.set({
1317                             "dd-test-val": trigger.value,
1318                             "cl-core-opt": generate_options(test["core"]),
1319                             "cl-core-val": list(),
1320                             "cl-core-all-val": list(),
1321                             "cl-core-all-opt": C.CL_ALL_ENABLED,
1322                             "cl-frmsize-opt": \
1323                                 generate_options(test["frame-size"]),
1324                             "cl-frmsize-val": list(),
1325                             "cl-frmsize-all-val": list(),
1326                             "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1327                             "cl-tsttype-opt": \
1328                                 generate_options(test["test-type"]),
1329                             "cl-tsttype-val": list(),
1330                             "cl-tsttype-all-val": list(),
1331                             "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1332                             "btn-add-dis": True
1333                         })
1334             elif trigger.type == "ctrl-cl":
1335                 param = trigger.idx.split("-")[0]
1336                 if "-all" in trigger.idx:
1337                     c_sel, c_all, c_id = list(), trigger.value, "all"
1338                 else:
1339                     c_sel, c_all, c_id = trigger.value, list(), str()
1340                 val_sel, val_all = sync_checklists(
1341                     options=ctrl_panel.get(f"cl-{param}-opt"),
1342                     sel=c_sel,
1343                     all=c_all,
1344                     id=c_id
1345                 )
1346                 ctrl_panel.set({
1347                     f"cl-{param}-val": val_sel,
1348                     f"cl-{param}-all-val": val_all,
1349                 })
1350                 if all((ctrl_panel.get("cl-core-val"), 
1351                         ctrl_panel.get("cl-frmsize-val"),
1352                         ctrl_panel.get("cl-tsttype-val"), )):
1353                     ctrl_panel.set({"btn-add-dis": False})
1354                 else:
1355                     ctrl_panel.set({"btn-add-dis": True})
1356             elif trigger.type == "ctrl-btn":
1357                 on_draw[0] = True
1358                 if trigger.idx == "add-test":
1359                     dut = ctrl_panel.get("dd-dut-val")
1360                     phy = ctrl_panel.get("dd-phy-val")
1361                     area = ctrl_panel.get("dd-area-val")
1362                     test = ctrl_panel.get("dd-test-val")
1363                     # Add selected test(s) to the list of tests in store:
1364                     if store_sel is None:
1365                         store_sel = list()
1366                     for core in ctrl_panel.get("cl-core-val"):
1367                         for framesize in ctrl_panel.get("cl-frmsize-val"):
1368                             for ttype in ctrl_panel.get("cl-tsttype-val"):
1369                                 if dut == "trex":
1370                                     core = str()
1371                                 tid = "-".join((
1372                                     dut,
1373                                     phy.replace('af_xdp', 'af-xdp'),
1374                                     area,
1375                                     framesize.lower(),
1376                                     core.lower(),
1377                                     test,
1378                                     ttype.lower()
1379                                 ))
1380                                 if tid not in [i["id"] for i in store_sel]:
1381                                     store_sel.append({
1382                                         "id": tid,
1383                                         "dut": dut,
1384                                         "phy": phy,
1385                                         "area": area,
1386                                         "test": test,
1387                                         "framesize": framesize.lower(),
1388                                         "core": core.lower(),
1389                                         "testtype": ttype.lower()
1390                                     })
1391                     store_sel = sorted(store_sel, key=lambda d: d["id"])
1392                     if C.CLEAR_ALL_INPUTS:
1393                         ctrl_panel.set(ctrl_panel.defaults)
1394                 elif trigger.idx == "rm-test" and lst_sel:
1395                     new_store_sel = list()
1396                     for idx, item in enumerate(store_sel):
1397                         if not lst_sel[idx]:
1398                             new_store_sel.append(item)
1399                     store_sel = new_store_sel
1400                 elif trigger.idx == "rm-test-all":
1401                     store_sel = list()
1402             elif trigger.type == "telemetry-btn":
1403                 if trigger.idx in ("open", "back"):
1404                     tm.from_dataframe(self._data)
1405                     tm_data = tm.to_json()
1406                     tm_user["unique_metrics"] = tm.unique_metrics
1407                     tm_user["selected_metrics"] = list()
1408                     tm_user["unique_labels"] = dict()
1409                     tm_user["selected_labels"] = dict()
1410                     search_out = (
1411                         get_list_group_items(tm_user["unique_metrics"],
1412                             "tele-cl", False),
1413                     )
1414                     is_open = (True, False)
1415                     tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1416                 elif trigger.idx == "select":
1417                     tm.from_json(tm_data)
1418                     if any(cl_metrics):
1419                         if not tm_user["selected_metrics"]:
1420                             tm_user["selected_metrics"] = \
1421                                 tm_user["unique_metrics"]
1422                         metrics = [a for a, b in \
1423                             zip(tm_user["selected_metrics"], cl_metrics) if b]
1424                         tm_user["selected_metrics"] = metrics
1425                         tm_user["unique_labels"] = \
1426                             tm.get_selected_labels(metrics)
1427                         tm_user["unique_metrics_with_labels"] = \
1428                             tm.unique_metrics_with_labels
1429                         list_metrics[0] = tm.str_metrics
1430                         tm_dd[0] = _get_dd_container(tm_user["unique_labels"])
1431                         if list_metrics[0]:
1432                             tm_btns_disabled[1] = True
1433                             tm_btns_disabled[4] = False
1434                         is_open = (False, True)
1435                     else:
1436                         tm_user = None
1437                         is_open = (False, False)
1438                 elif trigger.idx == "add":
1439                     tm.from_json(tm_data)
1440                     tm_panels.append(tm_user["selected_metrics_with_labels"])
1441                     tm_all_in_one.append(all_in_one)
1442                     is_open = (False, False)
1443                     tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1444                     on_draw = [True, True]
1445                 elif trigger.idx == "cancel":
1446                     is_open = (False, False)
1447                     tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1448                 elif trigger.idx == "rm-all":
1449                     tm_panels = list()
1450                     tm_all_in_one = list()
1451                     tm_user = None
1452                     is_open = (False, False)
1453                     tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1454                     plotting_area_telemetry = C.PLACEHOLDER
1455             elif trigger.type == "telemetry-search-in":
1456                 tm.from_metrics(tm_user["unique_metrics"])
1457                 tm_user["selected_metrics"] = \
1458                     tm.search_unique_metrics(trigger.value)
1459                 search_out = (get_list_group_items(
1460                     tm_user["selected_metrics"],
1461                     type="tele-cl",
1462                     colorize=False
1463                 ), )
1464                 is_open = (True, False)
1465             elif trigger.type == "tele-cl":
1466                 if any(cl_metrics):
1467                     tm_btns_disabled[1] = False
1468                 else:
1469                     tm_btns_disabled[1] = True
1470                 is_open = (True, False)
1471             elif trigger.type == "tm-dd":
1472                 tm.from_metrics_with_labels(
1473                     tm_user["unique_metrics_with_labels"]
1474                 )
1475                 selected = dict()
1476                 previous_itm = None
1477                 for itm in tm_dd_in:
1478                     if itm is None:
1479                         show_new = True
1480                     elif isinstance(itm, str):
1481                         show_new = False
1482                         selected[itm] = list()
1483                     elif isinstance(itm, list):
1484                         if previous_itm is not None:
1485                             selected[previous_itm] = itm
1486                         show_new = True
1487                     previous_itm = itm
1488                 tm_dd[0] = _get_dd_container(
1489                     tm_user["unique_labels"],
1490                     selected,
1491                     show_new
1492                 )
1493                 sel_metrics = tm.filter_selected_metrics_by_labels(selected)
1494                 tm_user["selected_metrics_with_labels"] = sel_metrics.to_dict()
1495                 if not sel_metrics.empty:
1496                     list_metrics[0] = tm.metrics_to_str(sel_metrics)
1497                     tm_btns_disabled[5] = False
1498                 else:
1499                     list_metrics[0] = str()
1500             elif trigger.type == "tm-btn-remove":
1501                 del tm_panels[trigger.idx]
1502                 del tm_all_in_one[trigger.idx]
1503                 tm.from_json(tm_data)
1504                 on_draw = [True, True]
1505
1506             new_url_params = {
1507                 "store_sel": store_sel,
1508                 "norm": ctrl_panel.get("cl-normalize-val")
1509             }
1510             if tm_panels:
1511                 new_url_params["telemetry"] = tm_panels
1512                 new_url_params["all-in-one"] = tm_all_in_one
1513
1514             if on_draw[0]:  # Trending
1515                 if store_sel:
1516                     lg_selected = get_list_group_items(store_sel, "sel-cl")
1517                     plotting_area_trending = self._plotting_area_trending(
1518                         store_sel,
1519                         bool(ctrl_panel.get("cl-normalize-val"))
1520                     )
1521                     if on_draw[1]:  # Telemetry
1522                         tm_graphs = list()
1523                         for panel, aio in zip(tm_panels, tm_all_in_one):
1524                             tm_graphs.append(graph_tm_trending(
1525                                 tm.select_tm_trending_data(panel),
1526                                 self._graph_layout,
1527                                 bool(aio[0])
1528                             ))
1529                         plotting_area_telemetry = \
1530                             Layout._plotting_area_telemetry(tm_graphs)
1531                     col_plotting_area = C.STYLE_ENABLED
1532                     row_card_sel_tests = C.STYLE_ENABLED
1533                     row_btns_sel_tests = C.STYLE_ENABLED
1534                     row_btns_add_tm = C.STYLE_ENABLED
1535                 else:
1536                     plotting_area_trending = no_update
1537                     plotting_area_telemetry = C.PLACEHOLDER
1538                     col_plotting_area = C.STYLE_DISABLED
1539                     row_card_sel_tests = C.STYLE_DISABLED
1540                     row_btns_sel_tests = C.STYLE_DISABLED
1541                     row_btns_add_tm = C.STYLE_DISABLED
1542                     lg_selected = no_update
1543                     store_sel = list()
1544                     tm_panels = list()
1545                     tm_all_in_one = list()
1546                     tm_user = None
1547             else:
1548                 plotting_area_trending = no_update
1549                 col_plotting_area = no_update
1550                 row_card_sel_tests = no_update
1551                 row_btns_sel_tests = no_update
1552                 row_btns_add_tm = no_update
1553                 lg_selected = no_update
1554
1555             store["url"] = gen_new_url(parsed_url, new_url_params)
1556             store["control-panel"] = ctrl_panel.panel
1557             store["selected-tests"] = store_sel
1558             store["telemetry-data"] = tm_data
1559             store["selected-metrics"] = tm_user
1560             store["telemetry-panels"] = tm_panels
1561             store["telemetry-all-in-one"] = tm_all_in_one
1562             ret_val = [
1563                 store,
1564                 plotting_area_trending,
1565                 plotting_area_telemetry,
1566                 col_plotting_area,
1567                 row_card_sel_tests,
1568                 row_btns_sel_tests,
1569                 row_btns_add_tm,
1570                 lg_selected,
1571                 search_out,
1572                 is_open,
1573                 tm_btns_disabled,
1574                 tm_dd,
1575                 list_metrics
1576             ]
1577             ret_val.extend(ctrl_panel.values)
1578             return ret_val
1579
1580         @app.callback(
1581             Output("plot-mod-url", "is_open"),
1582             Output("mod-url", "children"),
1583             State("store", "data"),
1584             State("plot-mod-url", "is_open"),
1585             Input("plot-btn-url", "n_clicks")
1586         )
1587         def toggle_plot_mod_url(store, is_open, n_clicks):
1588             """Toggle the modal window with url.
1589             """
1590             if not store:
1591                 raise PreventUpdate
1592
1593             if n_clicks:
1594                 return not is_open, store.get("url", str())
1595             return is_open, store["url"]
1596
1597         def _get_dd_container(
1598                 all_labels: dict,
1599                 selected_labels: dict=dict(),
1600                 show_new=True
1601             ) -> list:
1602             """Generate a container with dropdown selection boxes depenting on
1603             the input data.
1604
1605             :param all_labels: A dictionary with unique labels and their
1606                 possible values.
1607             :param selected_labels: A dictionalry with user selected lables and
1608                 their values.
1609             :param show_new: If True, a dropdown selection box to add a new
1610                 label is displayed.
1611             :type all_labels: dict
1612             :type selected_labels: dict
1613             :type show_new: bool
1614             :returns: A list of dbc rows with dropdown selection boxes.
1615             :rtype: list
1616             """
1617
1618             def _row(
1619                     id: str,
1620                     lopts: list=list(),
1621                     lval: str=str(),
1622                     vopts: list=list(),
1623                     vvals: list=list()
1624                 ) -> dbc.Row:
1625                 """Generates a dbc row with dropdown boxes.
1626
1627                 :param id: A string added to the dropdown ID.
1628                 :param lopts: A list of options for 'label' dropdown.
1629                 :param lval: Value of 'label' dropdown.
1630                 :param vopts: A list of options for 'value' dropdown.
1631                 :param vvals: A list of values for 'value' dropdown.
1632                 :type id: str
1633                 :type lopts: list
1634                 :type lval: str
1635                 :type vopts: list
1636                 :type vvals: list
1637                 :returns: dbc row with dropdown boxes.
1638                 :rtype: dbc.Row
1639                 """
1640                 children = list()
1641                 if lopts:
1642                     children.append(
1643                         dbc.Col(
1644                             width=6,
1645                             children=[
1646                                 dcc.Dropdown(
1647                                     id={
1648                                         "type": "tm-dd",
1649                                         "index": f"label-{id}"
1650                                     },
1651                                     placeholder="Select a label...",
1652                                     optionHeight=20,
1653                                     multi=False,
1654                                     options=lopts,
1655                                     value=lval if lval else None
1656                                 )
1657                             ]
1658                         )
1659                     )
1660                     if vopts:
1661                         children.append(
1662                             dbc.Col(
1663                                 width=6,
1664                                 children=[
1665                                     dcc.Dropdown(
1666                                         id={
1667                                             "type": "tm-dd",
1668                                             "index": f"value-{id}"
1669                                         },
1670                                         placeholder="Select a value...",
1671                                         optionHeight=20,
1672                                         multi=True,
1673                                         options=vopts,
1674                                         value=vvals if vvals else None
1675                                     )
1676                                 ]
1677                             )
1678                         )
1679
1680                 return dbc.Row(class_name="g-0 p-1", children=children)
1681
1682             container = list()
1683
1684             # Display rows with items in 'selected_labels'; label on the left,
1685             # values on the right:
1686             keys_left = list(all_labels.keys())
1687             for idx, label in enumerate(selected_labels.keys()):
1688                 container.append(_row(
1689                     id=idx,
1690                     lopts=deepcopy(keys_left),
1691                     lval=label,
1692                     vopts=all_labels[label],
1693                     vvals=selected_labels[label]
1694                 ))
1695                 keys_left.remove(label)
1696
1697             # Display row with dd with labels on the left, right side is empty:
1698             if show_new and keys_left:
1699                 container.append(_row(id="new", lopts=keys_left))
1700
1701             return container
1702
1703         @app.callback(
1704             Output("metadata-tput-lat", "children"),
1705             Output("metadata-hdrh-graph", "children"),
1706             Output("offcanvas-metadata", "is_open"),
1707             Input({"type": "graph", "index": ALL}, "clickData"),
1708             prevent_initial_call=True
1709         )
1710         def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1711             """Generates the data for the offcanvas displayed when a particular
1712             point in a graph is clicked on.
1713
1714             :param graph_data: The data from the clicked point in the graph.
1715             :type graph_data: dict
1716             :returns: The data to be displayed on the offcanvas and the
1717                 information to show the offcanvas.
1718             :rtype: tuple(list, list, bool)
1719             """
1720
1721             trigger = Trigger(callback_context.triggered)
1722
1723             try:
1724                 idx = 0 if trigger.idx == "tput" else 1
1725                 graph_data = graph_data[idx]["points"][0]
1726             except (IndexError, KeyError, ValueError, TypeError):
1727                 raise PreventUpdate
1728
1729             metadata = no_update
1730             graph = list()
1731
1732             children = [
1733                 dbc.ListGroupItem(
1734                     [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1735                 ) for x in graph_data.get("text", "").split("<br>")
1736             ]
1737             if trigger.idx == "tput":
1738                 title = "Throughput"
1739             elif trigger.idx == "lat":
1740                 title = "Latency"
1741                 hdrh_data = graph_data.get("customdata", None)
1742                 if hdrh_data:
1743                     graph = [dbc.Card(
1744                         class_name="gy-2 p-0",
1745                         children=[
1746                             dbc.CardHeader(hdrh_data.pop("name")),
1747                             dbc.CardBody(children=[
1748                                 dcc.Graph(
1749                                     id="hdrh-latency-graph",
1750                                     figure=graph_hdrh_latency(
1751                                         hdrh_data, self._graph_layout
1752                                     )
1753                                 )
1754                             ])
1755                         ])
1756                     ]
1757             else:
1758                 raise PreventUpdate
1759
1760             metadata = [
1761                 dbc.Card(
1762                     class_name="gy-2 p-0",
1763                     children=[
1764                         dbc.CardHeader(children=[
1765                             dcc.Clipboard(
1766                                 target_id="tput-lat-metadata",
1767                                 title="Copy",
1768                                 style={"display": "inline-block"}
1769                             ),
1770                             title
1771                         ]),
1772                         dbc.CardBody(
1773                             id="tput-lat-metadata",
1774                             class_name="p-0",
1775                             children=[dbc.ListGroup(children, flush=True), ]
1776                         )
1777                     ]
1778                 )
1779             ]
1780
1781             return metadata, graph, True
1782
1783         @app.callback(
1784             Output("download-trending-data", "data"),
1785             State("store", "data"),
1786             Input("plot-btn-download", "n_clicks"),
1787             Input({"type": "tm-btn-download", "index": ALL}, "n_clicks"),
1788             prevent_initial_call=True
1789         )
1790         def _download_data(store: list, *_) -> dict:
1791             """Download the data
1792
1793             :param store_sel: List of tests selected by user stored in the
1794                 browser.
1795             :type store_sel: list
1796             :returns: dict of data frame content (base64 encoded) and meta data
1797                 used by the Download component.
1798             :rtype: dict
1799             """
1800
1801             if not store:
1802                 raise PreventUpdate
1803             if not store["selected-tests"]:
1804                 raise PreventUpdate
1805             
1806             df = pd.DataFrame()
1807             
1808             trigger = Trigger(callback_context.triggered)
1809             if not trigger.value:
1810                 raise PreventUpdate
1811             
1812             if trigger.type == "plot-btn-download":
1813                 data = list()
1814                 for itm in store["selected-tests"]:
1815                     sel_data = select_trending_data(self._data, itm)
1816                     if sel_data is None:
1817                         continue
1818                     data.append(sel_data)
1819                 df = pd.concat(data, ignore_index=True, copy=False)
1820                 file_name = C.TREND_DOWNLOAD_FILE_NAME
1821             elif trigger.type == "tm-btn-download":
1822                 tm = TelemetryData(store["selected-tests"])
1823                 tm.from_json(store["telemetry-data"])
1824                 df = tm.select_tm_trending_data(
1825                     store["telemetry-panels"][trigger.idx]
1826                 )
1827                 file_name = C.TELEMETRY_DOWNLOAD_FILE_NAME
1828             else:
1829                 raise PreventUpdate
1830
1831             return dcc.send_data_frame(df.to_csv, file_name)