3a7cd946286a7025cf4eb0e43e689eea307ffa55
[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                         id={"type": "telemetry-btn", "index": "open"},
651                         children="Telemetry",
652                         color="info"
653                     ),
654                     dbc.Button(
655                         id="plot-btn-url",
656                         children="Show URL",
657                         color="info"
658                     ),
659                     dbc.Modal(
660                         [
661                             dbc.ModalHeader(dbc.ModalTitle("URL")),
662                             dbc.ModalBody(id="mod-url")
663                         ],
664                         id="plot-mod-url",
665                         size="xl",
666                         is_open=False,
667                         scrollable=True
668                     )
669                 ]
670             )
671         ]
672
673     def _add_plotting_col(self) -> dbc.Col:
674         """Add column with plots. It is placed on the right side.
675
676         :returns: Column with plots.
677         :rtype: dbc.Col
678         """
679         return dbc.Col(
680             id="col-plotting-area",
681             children=[
682                 dbc.Spinner(
683                     dbc.Row(
684                         id="plotting-area-trending",
685                         class_name="g-0 p-0",
686                         children=C.PLACEHOLDER
687                     ),
688                     delay_show=C.SPINNER_DELAY
689                 ),
690                 dbc.Row(
691                     id="plotting-area-telemetry",
692                     class_name="g-0 p-0",
693                     children=C.PLACEHOLDER
694                 )
695             ],
696             width=9,
697             style=C.STYLE_DISABLED,
698         )
699
700     def _plotting_area_trending(
701             self,
702             tests: list,
703             normalize: bool
704         ) -> dbc.Col:
705         """Generate the plotting area with all its content.
706
707         :param tests: A list of tests to be displayed in the trending graphs.
708         :param normalize: If True, the data in graphs is normalized.
709         :type tests: list
710         :type normalize: bool
711         :returns: A collumn with trending graphs (tput and latency) in tabs.
712         :rtype: dbc.Col
713         """
714         if not tests:
715             return C.PLACEHOLDER
716
717         figs = graph_trending(self._data, tests, self._graph_layout, normalize)
718
719         if not figs[0]:
720             return C.PLACEHOLDER
721
722         tab_items = [
723             dbc.Tab(
724                 children=dcc.Graph(
725                     id={"type": "graph", "index": "tput"},
726                     figure=figs[0]
727                 ),
728                 label="Throughput",
729                 tab_id="tab-tput"
730             )
731         ]
732
733         if figs[1]:
734             tab_items.append(
735                 dbc.Tab(
736                     children=dcc.Graph(
737                         id={"type": "graph", "index": "lat"},
738                         figure=figs[1]
739                     ),
740                     label="Latency",
741                     tab_id="tab-lat"
742                 )
743             )
744
745         trending = [
746             dbc.Row(children=[
747                 dbc.Tabs(
748                     children=tab_items,
749                     id="tabs",
750                     active_tab="tab-tput",
751                 )
752             ]),
753             dbc.Row(
754                 [
755                     dbc.Col([html.Div(
756                         [
757                             dbc.Button(
758                                 id="plot-btn-download",
759                                 children="Download Data",
760                                 class_name="me-1",
761                                 color="info",
762                                 style={"padding": "0rem 1rem"}
763                             ),
764                             dcc.Download(id="download-trending-data")
765                         ],
766                         className=\
767                             "d-grid gap-0 d-md-flex justify-content-md-end"
768                     )])
769                 ],
770                 class_name="g-0 p-0"
771             )
772         ]
773
774         return dbc.Col(
775             children=[
776                 dbc.Row(
777                     dbc.Accordion(
778                         children=[
779                             dbc.AccordionItem(
780                                 title="Trending",
781                                 children=trending
782                             )
783                         ],
784                         class_name="g-0 p-1",
785                         start_collapsed=False,
786                         always_open=True,
787                         active_item=["item-0", ]
788                     ),
789                     class_name="g-0 p-0",
790                 ),
791                 dbc.Modal(
792                     [
793                         dbc.ModalHeader(
794                             dbc.ModalTitle("Select a Metric"),
795                             close_button=False
796                         ),
797                         dbc.Spinner(
798                             dbc.ModalBody(self._get_telemetry_step_1()),
799                             delay_show=2 * C.SPINNER_DELAY
800                         ),
801                         dbc.ModalFooter([
802                             dbc.Button(
803                                 "Select",
804                                 id={"type": "telemetry-btn", "index": "select"},
805                                 disabled=True
806                             ),
807                             dbc.Button(
808                                 "Cancel",
809                                 id={"type": "telemetry-btn", "index": "cancel"},
810                                 disabled=False
811                             ),
812                             dbc.Button(
813                                 "Remove",
814                                 id={"type": "telemetry-btn", "index": "remove"},
815                                 disabled=False
816                             )
817                         ])
818                     ],
819                     id={"type": "plot-mod-telemetry", "index": 0},
820                     size="lg",
821                     is_open=False,
822                     scrollable=False,
823                     backdrop="static",
824                     keyboard=False
825                 ),
826                 dbc.Modal(
827                     [
828                         dbc.ModalHeader(
829                             dbc.ModalTitle("Select Labels"),
830                             close_button=False
831                         ),
832                         dbc.Spinner(
833                             dbc.ModalBody(self._get_telemetry_step_2()),
834                             delay_show=2 * C.SPINNER_DELAY
835                         ),
836                         dbc.ModalFooter([
837                             dbc.Button(
838                                 "Back",
839                                 id={"type": "telemetry-btn", "index": "back"},
840                                 disabled=False
841                             ),
842                             dbc.Button(
843                                 "Display Telemetry",
844                                 id={"type": "telemetry-btn", "index": "add"},
845                                 disabled=True
846                             ),
847                             dbc.Button(
848                                 "Cancel",
849                                 id={"type": "telemetry-btn", "index": "cancel"},
850                                 disabled=False
851                             )
852                         ])
853                     ],
854                     id={"type": "plot-mod-telemetry", "index": 1},
855                     size="xl",
856                     is_open=False,
857                     scrollable=False,
858                     backdrop="static",
859                     keyboard=False
860                 )
861             ]
862         )
863
864     def _plotting_area_telemetry(self, graphs: list) -> dbc.Col:
865         """Generate the plotting area with telemetry.
866         """
867         if not graphs:
868             return C.PLACEHOLDER
869
870         acc_items = list()
871         for graph in graphs:
872             acc_items.append(
873                 dbc.AccordionItem(
874                     title=f"Telemetry: {graph[1]}" if graph[1] else "Telemetry",
875                     children=dcc.Graph(
876                         id={"type": "graph-telemetry", "index": graph[1]},
877                         figure=graph[0]
878                     )
879                 )
880             )
881
882         return dbc.Col(
883             children=[
884                 dbc.Row(
885                     dbc.Accordion(
886                         children=acc_items,
887                         class_name="g-0 p-1",
888                         start_collapsed=False,
889                         always_open=True,
890                         active_item=[f"item-{i}" for i in range(len(acc_items))]
891                     ),
892                     class_name="g-0 p-0",
893                 )
894             ]
895         )
896
897     @staticmethod
898     def _get_telemetry_step_1() -> list:
899         """Return the content of the modal window used in the step 1 of metrics
900         selection.
901
902         :returns: A list of dbc rows with 'input' and 'search output'.
903         :rtype: list
904         """
905         return [
906             dbc.Row(
907                 class_name="g-0 p-1",
908                 children=[
909                     dbc.Input(
910                         id={"type": "telemetry-search-in", "index": 0},
911                         placeholder="Start typing a metric name...",
912                         type="text"
913                     )
914                 ]
915             ),
916             dbc.Row(
917                 class_name="g-0 p-1",
918                 children=[
919                     dbc.ListGroup(
920                         class_name="overflow-auto p-0",
921                         id={"type": "telemetry-search-out", "index": 0},
922                         children=[],
923                         style={"max-height": "14em"},
924                         flush=True
925                     )
926                 ]
927             )
928         ]
929
930     @staticmethod
931     def _get_telemetry_step_2() -> list:
932         """Return the content of the modal window used in the step 2 of metrics
933         selection.
934
935         :returns: A list of dbc rows with 'container with dynamic dropdowns' and
936             'search output'.
937         :rtype: list
938         """
939         return [
940             dbc.Row(
941                 id={"type": "tm-container", "index": 0},
942                 class_name="g-0 p-1",
943                 children=["Add content here."]
944             ),
945             dbc.Row(
946                 class_name="g-0 p-2",
947                 children=[
948                     dbc.Checkbox(
949                         id={"type": "cb-all-in-one", "index": 0},
950                         label="All Metrics in one Graph"
951                     ),
952                 ]
953             ),
954             dbc.Row(
955                 class_name="g-0 p-1",
956                 children=[
957                     dbc.Textarea(
958                         id={"type": "tm-list-metrics", "index": 0},
959                         rows=20,
960                         size="sm",
961                         wrap="off",
962                         readonly=True
963                     )
964                 ]
965             )
966         ]
967
968     def callbacks(self, app):
969         """Callbacks for the whole application.
970
971         :param app: The application.
972         :type app: Flask
973         """
974
975         @app.callback(
976             Output("store", "data"),
977             Output("plotting-area-trending", "children"),
978             Output("plotting-area-telemetry", "children"),
979             Output("col-plotting-area", "style"),
980             Output("row-card-sel-tests", "style"),
981             Output("row-btns-sel-tests", "style"),
982             Output("row-btns-add-tm", "style"),
983             Output("lg-selected", "children"),
984             Output({"type": "telemetry-search-out", "index": ALL}, "children"),
985             Output({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
986             Output({"type": "telemetry-btn", "index": ALL}, "disabled"),
987             Output({"type": "tm-container", "index": ALL}, "children"),
988             Output({"type": "tm-list-metrics", "index": ALL}, "value"),
989             Output({"type": "ctrl-dd", "index": "dut"}, "value"),
990             Output({"type": "ctrl-dd", "index": "phy"}, "options"),
991             Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
992             Output({"type": "ctrl-dd", "index": "phy"}, "value"),
993             Output({"type": "ctrl-dd", "index": "area"}, "options"),
994             Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
995             Output({"type": "ctrl-dd", "index": "area"}, "value"),
996             Output({"type": "ctrl-dd", "index": "test"}, "options"),
997             Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
998             Output({"type": "ctrl-dd", "index": "test"}, "value"),
999             Output({"type": "ctrl-cl", "index": "core"}, "options"),
1000             Output({"type": "ctrl-cl", "index": "core"}, "value"),
1001             Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
1002             Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
1003             Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
1004             Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
1005             Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
1006             Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
1007             Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
1008             Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
1009             Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
1010             Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
1011             Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
1012             Output("normalize", "value"),
1013
1014             State("store", "data"),
1015             State({"type": "sel-cl", "index": ALL}, "value"),
1016             State({"type": "cb-all-in-one", "index": ALL}, "value"),
1017             State({"type": "telemetry-search-out", "index": ALL}, "children"),
1018             State({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
1019             State({"type": "telemetry-btn", "index": ALL}, "disabled"),
1020             State({"type": "tm-container", "index": ALL}, "children"),
1021             State({"type": "tm-list-metrics", "index": ALL}, "value"),
1022
1023             Input("url", "href"),
1024             Input({"type": "tele-cl", "index": ALL}, "value"),
1025             Input({"type": "tm-dd", "index": ALL}, "value"),
1026
1027             Input("normalize", "value"),
1028             Input({"type": "telemetry-search-in", "index": ALL}, "value"),
1029             Input({"type": "telemetry-btn", "index": ALL}, "n_clicks"),
1030             Input({"type": "ctrl-dd", "index": ALL}, "value"),
1031             Input({"type": "ctrl-cl", "index": ALL}, "value"),
1032             Input({"type": "ctrl-btn", "index": ALL}, "n_clicks"),
1033
1034             prevent_initial_call=True
1035         )
1036         def _update_application(
1037                 store: dict,
1038                 lst_sel: list,
1039                 all_in_one: list,
1040                 search_out: list,
1041                 is_open: list,
1042                 tm_btns_disabled: list,
1043                 tm_dd: list,
1044                 list_metrics: list,
1045                 href: str,
1046                 cl_metrics: list,
1047                 tm_dd_in: list,
1048                 *_
1049             ) -> tuple:
1050             """Update the application when the event is detected.
1051             """
1052
1053             if store is None:
1054                 store = {
1055                     "control-panel": dict(),
1056                     "selected-tests": list(),
1057                     "telemetry-data": dict(),
1058                     "selected-metrics": dict(),
1059                     "url": str()
1060                 }
1061
1062             ctrl_panel = ControlPanel(
1063                 CP_PARAMS,
1064                 store.get("control-panel", dict())
1065             )
1066             store_sel = store["selected-tests"]
1067             tm_data = store["telemetry-data"]
1068             tm_user = store["selected-metrics"]
1069
1070             plotting_area_telemetry = no_update
1071             on_draw = [False, False]  # 0 --> trending, 1 --> telemetry
1072
1073             # Parse the url:
1074             parsed_url = url_decode(href)
1075             if parsed_url:
1076                 url_params = parsed_url["params"]
1077             else:
1078                 url_params = None
1079
1080             if tm_user is None:
1081                 # Telemetry user data
1082                 # The data provided by user or result of user action
1083                 tm_user = {
1084                     # List of unique metrics:
1085                     "unique_metrics": list(),
1086                     # List of metrics selected by user:
1087                     "selected_metrics": list(),
1088                     # Labels from metrics selected by user (key: label name,
1089                     # value: list of all possible values):
1090                     "unique_labels": dict(),
1091                     # Labels selected by the user (subset of 'unique_labels'):
1092                     "selected_labels": dict(),
1093                     # All unique metrics with labels (output from the step 1)
1094                     # converted from pandas dataframe to dictionary.
1095                     "unique_metrics_with_labels": dict(),
1096                     # Metrics with labels selected by the user using dropdowns.
1097                     "selected_metrics_with_labels": dict()
1098                 }
1099             tm = TelemetryData(store_sel) if store_sel else TelemetryData()
1100
1101             trigger = Trigger(callback_context.triggered)
1102             if trigger.type == "url" and url_params:
1103                 telemetry = None
1104                 try:
1105                     store_sel = literal_eval(url_params["store_sel"][0])
1106                     normalize = literal_eval(url_params["norm"][0])
1107                     telemetry = literal_eval(url_params["telemetry"][0])
1108                     all_in_one = literal_eval(url_params["all-in-one"][0])
1109                 except (KeyError, IndexError, AttributeError):
1110                     pass
1111                 if store_sel:
1112                     last_test = store_sel[-1]
1113                     test = self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1114                         [last_test["area"]][last_test["test"]]
1115                     ctrl_panel.set({
1116                         "dd-dut-val": last_test["dut"],
1117                         "dd-phy-val": last_test["phy"],
1118                         "dd-phy-opt": generate_options(
1119                             self._spec_tbs[last_test["dut"]].keys()
1120                         ),
1121                         "dd-phy-dis": False,
1122                         "dd-area-val": last_test["area"],
1123                         "dd-area-opt": [
1124                             {"label": label(v), "value": v} for v in sorted(
1125                                 self._spec_tbs[last_test["dut"]]\
1126                                     [last_test["phy"]].keys()
1127                             )
1128                         ],
1129                         "dd-area-dis": False,
1130                         "dd-test-val": last_test["test"],
1131                         "dd-test-opt": generate_options(
1132                             self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1133                                 [last_test["area"]].keys()
1134                         ),
1135                         "dd-test-dis": False,
1136                         "cl-core-opt": generate_options(test["core"]),
1137                         "cl-core-val": [last_test["core"].upper(), ],
1138                         "cl-core-all-val": list(),
1139                         "cl-core-all-opt": C.CL_ALL_ENABLED,
1140                         "cl-frmsize-opt": generate_options(test["frame-size"]),
1141                         "cl-frmsize-val": [last_test["framesize"].upper(), ],
1142                         "cl-frmsize-all-val": list(),
1143                         "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1144                         "cl-tsttype-opt": generate_options(test["test-type"]),
1145                         "cl-tsttype-val": [last_test["testtype"].upper(), ],
1146                         "cl-tsttype-all-val": list(),
1147                         "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1148                         "cl-normalize-val": normalize,
1149                         "btn-add-dis": False
1150                     })
1151                     on_draw[0] = True
1152                     if telemetry:
1153                         tm = TelemetryData(store_sel)
1154                         tm.from_dataframe(self._data)
1155                         tm_data = tm.to_json()
1156                         tm.from_json(tm_data)
1157                         tm_user["selected_metrics_with_labels"] = telemetry
1158                         on_draw[1] = True
1159             elif trigger.type == "normalize":
1160                 ctrl_panel.set({"cl-normalize-val": trigger.value})
1161                 on_draw[0] = True
1162             elif trigger.type == "ctrl-dd":
1163                 if trigger.idx == "dut":
1164                     try:
1165                         options = generate_options(
1166                             self._spec_tbs[trigger.value].keys()
1167                         )
1168                         disabled = False
1169                     except KeyError:
1170                         options = list()
1171                         disabled = True
1172                     ctrl_panel.set({
1173                         "dd-dut-val": trigger.value,
1174                         "dd-phy-val": str(),
1175                         "dd-phy-opt": options,
1176                         "dd-phy-dis": disabled,
1177                         "dd-area-val": str(),
1178                         "dd-area-opt": list(),
1179                         "dd-area-dis": True,
1180                         "dd-test-val": str(),
1181                         "dd-test-opt": list(),
1182                         "dd-test-dis": True,
1183                         "cl-core-opt": list(),
1184                         "cl-core-val": list(),
1185                         "cl-core-all-val": list(),
1186                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1187                         "cl-frmsize-opt": list(),
1188                         "cl-frmsize-val": list(),
1189                         "cl-frmsize-all-val": list(),
1190                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1191                         "cl-tsttype-opt": list(),
1192                         "cl-tsttype-val": list(),
1193                         "cl-tsttype-all-val": list(),
1194                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1195                         "btn-add-dis": True
1196                     })
1197                 elif trigger.idx == "phy":
1198                     try:
1199                         dut = ctrl_panel.get("dd-dut-val")
1200                         phy = self._spec_tbs[dut][trigger.value]
1201                         options = [{"label": label(v), "value": v} \
1202                             for v in sorted(phy.keys())]
1203                         disabled = False
1204                     except KeyError:
1205                         options = list()
1206                         disabled = True
1207                     ctrl_panel.set({
1208                         "dd-phy-val": trigger.value,
1209                         "dd-area-val": str(),
1210                         "dd-area-opt": options,
1211                         "dd-area-dis": disabled,
1212                         "dd-test-val": str(),
1213                         "dd-test-opt": list(),
1214                         "dd-test-dis": True,
1215                         "cl-core-opt": list(),
1216                         "cl-core-val": list(),
1217                         "cl-core-all-val": list(),
1218                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1219                         "cl-frmsize-opt": list(),
1220                         "cl-frmsize-val": list(),
1221                         "cl-frmsize-all-val": list(),
1222                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1223                         "cl-tsttype-opt": list(),
1224                         "cl-tsttype-val": list(),
1225                         "cl-tsttype-all-val": list(),
1226                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1227                         "btn-add-dis": True
1228                     })  
1229                 elif trigger.idx == "area":
1230                     try:
1231                         dut = ctrl_panel.get("dd-dut-val")
1232                         phy = ctrl_panel.get("dd-phy-val")
1233                         area = self._spec_tbs[dut][phy][trigger.value]
1234                         options = generate_options(area.keys())
1235                         disabled = False
1236                     except KeyError:
1237                         options = list()
1238                         disabled = True
1239                     ctrl_panel.set({
1240                         "dd-area-val": trigger.value,
1241                         "dd-test-val": str(),
1242                         "dd-test-opt": options,
1243                         "dd-test-dis": disabled,
1244                         "cl-core-opt": list(),
1245                         "cl-core-val": list(),
1246                         "cl-core-all-val": list(),
1247                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1248                         "cl-frmsize-opt": list(),
1249                         "cl-frmsize-val": list(),
1250                         "cl-frmsize-all-val": list(),
1251                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1252                         "cl-tsttype-opt": list(),
1253                         "cl-tsttype-val": list(),
1254                         "cl-tsttype-all-val": list(),
1255                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1256                         "btn-add-dis": True
1257                     })
1258                 elif trigger.idx == "test":
1259                     dut = ctrl_panel.get("dd-dut-val")
1260                     phy = ctrl_panel.get("dd-phy-val")
1261                     area = ctrl_panel.get("dd-area-val")
1262                     if all((dut, phy, area, trigger.value, )):
1263                         test = self._spec_tbs[dut][phy][area][trigger.value]
1264                         ctrl_panel.set({
1265                             "dd-test-val": trigger.value,
1266                             "cl-core-opt": generate_options(test["core"]),
1267                             "cl-core-val": list(),
1268                             "cl-core-all-val": list(),
1269                             "cl-core-all-opt": C.CL_ALL_ENABLED,
1270                             "cl-frmsize-opt": \
1271                                 generate_options(test["frame-size"]),
1272                             "cl-frmsize-val": list(),
1273                             "cl-frmsize-all-val": list(),
1274                             "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1275                             "cl-tsttype-opt": \
1276                                 generate_options(test["test-type"]),
1277                             "cl-tsttype-val": list(),
1278                             "cl-tsttype-all-val": list(),
1279                             "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1280                             "btn-add-dis": True
1281                         })
1282             elif trigger.type == "ctrl-cl":
1283                 param = trigger.idx.split("-")[0]
1284                 if "-all" in trigger.idx:
1285                     c_sel, c_all, c_id = list(), trigger.value, "all"
1286                 else:
1287                     c_sel, c_all, c_id = trigger.value, list(), str()
1288                 val_sel, val_all = sync_checklists(
1289                     options=ctrl_panel.get(f"cl-{param}-opt"),
1290                     sel=c_sel,
1291                     all=c_all,
1292                     id=c_id
1293                 )
1294                 ctrl_panel.set({
1295                     f"cl-{param}-val": val_sel,
1296                     f"cl-{param}-all-val": val_all,
1297                 })
1298                 if all((ctrl_panel.get("cl-core-val"), 
1299                         ctrl_panel.get("cl-frmsize-val"),
1300                         ctrl_panel.get("cl-tsttype-val"), )):
1301                     ctrl_panel.set({"btn-add-dis": False})
1302                 else:
1303                     ctrl_panel.set({"btn-add-dis": True})
1304             elif trigger.type == "ctrl-btn":
1305                 on_draw[0] = True
1306                 if trigger.idx == "add-test":
1307                     dut = ctrl_panel.get("dd-dut-val")
1308                     phy = ctrl_panel.get("dd-phy-val")
1309                     area = ctrl_panel.get("dd-area-val")
1310                     test = ctrl_panel.get("dd-test-val")
1311                     # Add selected test(s) to the list of tests in store:
1312                     if store_sel is None:
1313                         store_sel = list()
1314                     for core in ctrl_panel.get("cl-core-val"):
1315                         for framesize in ctrl_panel.get("cl-frmsize-val"):
1316                             for ttype in ctrl_panel.get("cl-tsttype-val"):
1317                                 if dut == "trex":
1318                                     core = str()
1319                                 tid = "-".join((
1320                                     dut,
1321                                     phy.replace('af_xdp', 'af-xdp'),
1322                                     area,
1323                                     framesize.lower(),
1324                                     core.lower(),
1325                                     test,
1326                                     ttype.lower()
1327                                 ))
1328                                 if tid not in [i["id"] for i in store_sel]:
1329                                     store_sel.append({
1330                                         "id": tid,
1331                                         "dut": dut,
1332                                         "phy": phy,
1333                                         "area": area,
1334                                         "test": test,
1335                                         "framesize": framesize.lower(),
1336                                         "core": core.lower(),
1337                                         "testtype": ttype.lower()
1338                                     })
1339                     store_sel = sorted(store_sel, key=lambda d: d["id"])
1340                     if C.CLEAR_ALL_INPUTS:
1341                         ctrl_panel.set(ctrl_panel.defaults)
1342                 elif trigger.idx == "rm-test" and lst_sel:
1343                     new_store_sel = list()
1344                     for idx, item in enumerate(store_sel):
1345                         if not lst_sel[idx]:
1346                             new_store_sel.append(item)
1347                     store_sel = new_store_sel
1348                 elif trigger.idx == "rm-test-all":
1349                     store_sel = list()
1350             elif trigger.type == "telemetry-btn":
1351                 if trigger.idx in ("open", "back"):
1352                     tm.from_dataframe(self._data)
1353                     tm_data = tm.to_json()
1354                     tm_user["unique_metrics"] = tm.unique_metrics
1355                     tm_user["selected_metrics"] = list()
1356                     tm_user["unique_labels"] = dict()
1357                     tm_user["selected_labels"] = dict()
1358                     search_out = (
1359                         get_list_group_items(tm_user["unique_metrics"],
1360                             "tele-cl", False),
1361                     )
1362                     is_open = (True, False)
1363                     tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1364                 elif trigger.idx == "select":
1365                     tm.from_json(tm_data)
1366                     if any(cl_metrics):
1367                         if not tm_user["selected_metrics"]:
1368                             tm_user["selected_metrics"] = \
1369                                 tm_user["unique_metrics"]
1370                         metrics = [a for a, b in \
1371                             zip(tm_user["selected_metrics"], cl_metrics) if b]
1372                         tm_user["selected_metrics"] = metrics
1373                         tm_user["unique_labels"] = \
1374                             tm.get_selected_labels(metrics)
1375                         tm_user["unique_metrics_with_labels"] = \
1376                             tm.unique_metrics_with_labels
1377                         list_metrics[0] = tm.str_metrics
1378                         tm_dd[0] = _get_dd_container(tm_user["unique_labels"])
1379                         if list_metrics[0]:
1380                             tm_btns_disabled[1] = True
1381                             tm_btns_disabled[4] = False
1382                         is_open = (False, True)
1383                     else:
1384                         tm_user = None
1385                         is_open = (False, False)
1386                 elif trigger.idx == "add":
1387                     tm.from_json(tm_data)
1388                     is_open = (False, False)
1389                     tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1390                     on_draw = [True, True]
1391                 elif trigger.idx == "cancel":
1392                     is_open = (False, False)
1393                     tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1394                 elif trigger.idx == "remove":
1395                     tm_user = None
1396                     is_open = (False, False)
1397                     tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1398                     plotting_area_telemetry = C.PLACEHOLDER
1399             elif trigger.type == "telemetry-search-in":
1400                 tm.from_metrics(tm_user["unique_metrics"])
1401                 tm_user["selected_metrics"] = \
1402                     tm.search_unique_metrics(trigger.value)
1403                 search_out = (get_list_group_items(
1404                     tm_user["selected_metrics"],
1405                     type="tele-cl",
1406                     colorize=False
1407                 ), )
1408                 is_open = (True, False)
1409             elif trigger.type == "tele-cl":
1410                 if any(cl_metrics):
1411                     tm_btns_disabled[1] = False
1412                 else:
1413                     tm_btns_disabled[1] = True
1414                 is_open = (True, False)
1415             elif trigger.type == "tm-dd":
1416                 tm.from_metrics_with_labels(
1417                     tm_user["unique_metrics_with_labels"]
1418                 )
1419                 selected = dict()
1420                 previous_itm = None
1421                 for itm in tm_dd_in:
1422                     if itm is None:
1423                         show_new = True
1424                     elif isinstance(itm, str):
1425                         show_new = False
1426                         selected[itm] = list()
1427                     elif isinstance(itm, list):
1428                         if previous_itm is not None:
1429                             selected[previous_itm] = itm
1430                         show_new = True
1431                     previous_itm = itm
1432                 tm_dd[0] = _get_dd_container(
1433                     tm_user["unique_labels"],
1434                     selected,
1435                     show_new
1436                 )
1437                 sel_metrics = tm.filter_selected_metrics_by_labels(selected)
1438                 tm_user["selected_metrics_with_labels"] = sel_metrics.to_dict()
1439                 if not sel_metrics.empty:
1440                     list_metrics[0] = tm.metrics_to_str(sel_metrics)
1441                     tm_btns_disabled[5] = False
1442                 else:
1443                     list_metrics[0] = str()
1444
1445             new_url_params = {
1446                 "store_sel": store_sel,
1447                 "norm": ctrl_panel.get("cl-normalize-val")
1448             }
1449             if tm_user and tm_user.get("selected_metrics_with_labels", None):
1450                 new_url_params["telemetry"] = \
1451                     tm_user["selected_metrics_with_labels"]
1452                 new_url_params["all-in-one"] = all_in_one
1453
1454             if on_draw[0]:  # Trending
1455                 if store_sel:
1456                     lg_selected = get_list_group_items(store_sel, "sel-cl")
1457                     plotting_area_trending = self._plotting_area_trending(
1458                         store_sel,
1459                         bool(ctrl_panel.get("cl-normalize-val"))
1460                     )
1461                     if on_draw[1]:  # Telemetry
1462                         plotting_area_telemetry = self._plotting_area_telemetry(
1463                             graph_tm_trending(
1464                                 tm.select_tm_trending_data(
1465                                     tm_user["selected_metrics_with_labels"]
1466                                 ),
1467                                 self._graph_layout,
1468                                 False if not all_in_one else all_in_one[0]
1469                             )
1470                         )
1471                     col_plotting_area = C.STYLE_ENABLED
1472                     row_card_sel_tests = C.STYLE_ENABLED
1473                     row_btns_sel_tests = C.STYLE_ENABLED
1474                     row_btns_add_tm = C.STYLE_ENABLED
1475                 else:
1476                     plotting_area_trending = no_update
1477                     plotting_area_telemetry = C.PLACEHOLDER
1478                     col_plotting_area = C.STYLE_DISABLED
1479                     row_card_sel_tests = C.STYLE_DISABLED
1480                     row_btns_sel_tests = C.STYLE_DISABLED
1481                     row_btns_add_tm = C.STYLE_DISABLED
1482                     lg_selected = no_update
1483                     store_sel = list()
1484                     tm_user = None
1485             else:
1486                 plotting_area_trending = no_update
1487                 col_plotting_area = no_update
1488                 row_card_sel_tests = no_update
1489                 row_btns_sel_tests = no_update
1490                 row_btns_add_tm = no_update
1491                 lg_selected = no_update
1492
1493             store["url"] = gen_new_url(parsed_url, new_url_params)
1494             store["control-panel"] = ctrl_panel.panel
1495             store["selected-tests"] = store_sel
1496             store["telemetry-data"] = tm_data
1497             store["selected-metrics"] = tm_user
1498             ret_val = [
1499                 store,
1500                 plotting_area_trending,
1501                 plotting_area_telemetry,
1502                 col_plotting_area,
1503                 row_card_sel_tests,
1504                 row_btns_sel_tests,
1505                 row_btns_add_tm,
1506                 lg_selected,
1507                 search_out,
1508                 is_open,
1509                 tm_btns_disabled,
1510                 tm_dd,
1511                 list_metrics
1512             ]
1513             ret_val.extend(ctrl_panel.values)
1514             return ret_val
1515
1516         @app.callback(
1517             Output("plot-mod-url", "is_open"),
1518             Output("mod-url", "children"),
1519             State("store", "data"),
1520             State("plot-mod-url", "is_open"),
1521             Input("plot-btn-url", "n_clicks")
1522         )
1523         def toggle_plot_mod_url(store, is_open, n_clicks):
1524             """Toggle the modal window with url.
1525             """
1526             if not store:
1527                 raise PreventUpdate
1528
1529             if n_clicks:
1530                 return not is_open, store.get("url", str())
1531             return is_open, store["url"]
1532
1533         def _get_dd_container(
1534                 all_labels: dict,
1535                 selected_labels: dict=dict(),
1536                 show_new=True
1537             ) -> list:
1538             """Generate a container with dropdown selection boxes depenting on
1539             the input data.
1540
1541             :param all_labels: A dictionary with unique labels and their
1542                 possible values.
1543             :param selected_labels: A dictionalry with user selected lables and
1544                 their values.
1545             :param show_new: If True, a dropdown selection box to add a new
1546                 label is displayed.
1547             :type all_labels: dict
1548             :type selected_labels: dict
1549             :type show_new: bool
1550             :returns: A list of dbc rows with dropdown selection boxes.
1551             :rtype: list
1552             """
1553
1554             def _row(
1555                     id: str,
1556                     lopts: list=list(),
1557                     lval: str=str(),
1558                     vopts: list=list(),
1559                     vvals: list=list()
1560                 ) -> dbc.Row:
1561                 """Generates a dbc row with dropdown boxes.
1562
1563                 :param id: A string added to the dropdown ID.
1564                 :param lopts: A list of options for 'label' dropdown.
1565                 :param lval: Value of 'label' dropdown.
1566                 :param vopts: A list of options for 'value' dropdown.
1567                 :param vvals: A list of values for 'value' dropdown.
1568                 :type id: str
1569                 :type lopts: list
1570                 :type lval: str
1571                 :type vopts: list
1572                 :type vvals: list
1573                 :returns: dbc row with dropdown boxes.
1574                 :rtype: dbc.Row
1575                 """
1576                 children = list()
1577                 if lopts:
1578                     children.append(
1579                         dbc.Col(
1580                             width=6,
1581                             children=[
1582                                 dcc.Dropdown(
1583                                     id={
1584                                         "type": "tm-dd",
1585                                         "index": f"label-{id}"
1586                                     },
1587                                     placeholder="Select a label...",
1588                                     optionHeight=20,
1589                                     multi=False,
1590                                     options=lopts,
1591                                     value=lval if lval else None
1592                                 )
1593                             ]
1594                         )
1595                     )
1596                     if vopts:
1597                         children.append(
1598                             dbc.Col(
1599                                 width=6,
1600                                 children=[
1601                                     dcc.Dropdown(
1602                                         id={
1603                                             "type": "tm-dd",
1604                                             "index": f"value-{id}"
1605                                         },
1606                                         placeholder="Select a value...",
1607                                         optionHeight=20,
1608                                         multi=True,
1609                                         options=vopts,
1610                                         value=vvals if vvals else None
1611                                     )
1612                                 ]
1613                             )
1614                         )
1615
1616                 return dbc.Row(class_name="g-0 p-1", children=children)
1617
1618             container = list()
1619
1620             # Display rows with items in 'selected_labels'; label on the left,
1621             # values on the right:
1622             keys_left = list(all_labels.keys())
1623             for idx, label in enumerate(selected_labels.keys()):
1624                 container.append(_row(
1625                     id=idx,
1626                     lopts=deepcopy(keys_left),
1627                     lval=label,
1628                     vopts=all_labels[label],
1629                     vvals=selected_labels[label]
1630                 ))
1631                 keys_left.remove(label)
1632
1633             # Display row with dd with labels on the left, right side is empty:
1634             if show_new and keys_left:
1635                 container.append(_row(id="new", lopts=keys_left))
1636
1637             return container
1638
1639         @app.callback(
1640             Output("metadata-tput-lat", "children"),
1641             Output("metadata-hdrh-graph", "children"),
1642             Output("offcanvas-metadata", "is_open"),
1643             Input({"type": "graph", "index": ALL}, "clickData"),
1644             prevent_initial_call=True
1645         )
1646         def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1647             """Generates the data for the offcanvas displayed when a particular
1648             point in a graph is clicked on.
1649
1650             :param graph_data: The data from the clicked point in the graph.
1651             :type graph_data: dict
1652             :returns: The data to be displayed on the offcanvas and the
1653                 information to show the offcanvas.
1654             :rtype: tuple(list, list, bool)
1655             """
1656
1657             trigger = Trigger(callback_context.triggered)
1658
1659             try:
1660                 idx = 0 if trigger.idx == "tput" else 1
1661                 graph_data = graph_data[idx]["points"][0]
1662             except (IndexError, KeyError, ValueError, TypeError):
1663                 raise PreventUpdate
1664
1665             metadata = no_update
1666             graph = list()
1667
1668             children = [
1669                 dbc.ListGroupItem(
1670                     [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1671                 ) for x in graph_data.get("text", "").split("<br>")
1672             ]
1673             if trigger.idx == "tput":
1674                 title = "Throughput"
1675             elif trigger.idx == "lat":
1676                 title = "Latency"
1677                 hdrh_data = graph_data.get("customdata", None)
1678                 if hdrh_data:
1679                     graph = [dbc.Card(
1680                         class_name="gy-2 p-0",
1681                         children=[
1682                             dbc.CardHeader(hdrh_data.pop("name")),
1683                             dbc.CardBody(children=[
1684                                 dcc.Graph(
1685                                     id="hdrh-latency-graph",
1686                                     figure=graph_hdrh_latency(
1687                                         hdrh_data, self._graph_layout
1688                                     )
1689                                 )
1690                             ])
1691                         ])
1692                     ]
1693             else:
1694                 raise PreventUpdate
1695
1696             metadata = [
1697                 dbc.Card(
1698                     class_name="gy-2 p-0",
1699                     children=[
1700                         dbc.CardHeader(children=[
1701                             dcc.Clipboard(
1702                                 target_id="tput-lat-metadata",
1703                                 title="Copy",
1704                                 style={"display": "inline-block"}
1705                             ),
1706                             title
1707                         ]),
1708                         dbc.CardBody(
1709                             id="tput-lat-metadata",
1710                             class_name="p-0",
1711                             children=[dbc.ListGroup(children, flush=True), ]
1712                         )
1713                     ]
1714                 )
1715             ]
1716
1717             return metadata, graph, True
1718
1719         @app.callback(
1720             Output("download-trending-data", "data"),
1721             State("store", "data"),
1722             Input("plot-btn-download", "n_clicks"),
1723             prevent_initial_call=True
1724         )
1725         def _download_trending_data(store: list, _) -> dict:
1726             """Download the data
1727
1728             :param store_sel: List of tests selected by user stored in the
1729                 browser.
1730             :type store_sel: list
1731             :returns: dict of data frame content (base64 encoded) and meta data
1732                 used by the Download component.
1733             :rtype: dict
1734             """
1735
1736             if not store:
1737                 raise PreventUpdate
1738             if not store["selected-tests"]:
1739                 raise PreventUpdate
1740
1741             df = pd.DataFrame()
1742             for itm in store["selected-tests"]:
1743                 sel_data = select_trending_data(self._data, itm)
1744                 if sel_data is None:
1745                     continue
1746                 df = pd.concat([df, sel_data], ignore_index=True, copy=False)
1747
1748             return dcc.send_data_frame(df.to_csv, C.TREND_DOWNLOAD_FILE_NAME)