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