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