74c39eb8c05da784814d908730fc3b1275a1bdd8
[csit.git] / csit.infra.dash / app / cdash / trending / layout.py
1 # Copyright (c) 2023 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14
15 """Plotly Dash HTML layout override.
16 """
17
18 import logging
19 import pandas as pd
20 import dash_bootstrap_components as dbc
21
22 from flask import Flask
23 from dash import dcc
24 from dash import html
25 from dash import callback_context, no_update, ALL
26 from dash import Input, Output, State
27 from dash.exceptions import PreventUpdate
28 from yaml import load, FullLoader, YAMLError
29 from ast import literal_eval
30 from copy import deepcopy
31
32 from ..utils.constants import Constants as C
33 from ..utils.control_panel import ControlPanel
34 from ..utils.trigger import Trigger
35 from ..utils.telemetry_data import TelemetryData
36 from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
37     generate_options, get_list_group_items, graph_hdrh_latency
38 from ..utils.url_processing import url_decode
39 from .graphs import graph_trending, select_trending_data, graph_tm_trending
40
41
42 # Control panel partameters and their default values.
43 CP_PARAMS = {
44     "dd-dut-val": str(),
45     "dd-phy-opt": list(),
46     "dd-phy-dis": True,
47     "dd-phy-val": str(),
48     "dd-area-opt": list(),
49     "dd-area-dis": True,
50     "dd-area-val": str(),
51     "dd-test-opt": list(),
52     "dd-test-dis": True,
53     "dd-test-val": str(),
54     "cl-core-opt": list(),
55     "cl-core-val": list(),
56     "cl-core-all-val": list(),
57     "cl-core-all-opt": C.CL_ALL_DISABLED,
58     "cl-frmsize-opt": list(),
59     "cl-frmsize-val": list(),
60     "cl-frmsize-all-val": list(),
61     "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
62     "cl-tsttype-opt": list(),
63     "cl-tsttype-val": list(),
64     "cl-tsttype-all-val": list(),
65     "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
66     "btn-add-dis": True,
67     "cl-normalize-val": list()
68 }
69
70
71 class Layout:
72     """The layout of the dash app and the callbacks.
73     """
74
75     def __init__(self,
76             app: Flask,
77             data_trending: pd.DataFrame,
78             html_layout_file: str,
79             graph_layout_file: str,
80             tooltip_file: str
81         ) -> None:
82         """Initialization:
83         - save the input parameters,
84         - read and pre-process the data,
85         - prepare data for the control panel,
86         - read HTML layout file,
87         - read tooltips from the tooltip file.
88
89         :param app: Flask application running the dash application.
90         :param data_trending: Pandas dataframe with trending data.
91         :param html_layout_file: Path and name of the file specifying the HTML
92             layout of the dash application.
93         :param graph_layout_file: Path and name of the file with layout of
94             plot.ly graphs.
95         :param tooltip_file: Path and name of the yaml file specifying the
96             tooltips.
97         :type app: Flask
98         :type data_trending: pandas.DataFrame
99         :type html_layout_file: str
100         :type graph_layout_file: str
101         :type tooltip_file: str
102         """
103
104         # Inputs
105         self._app = app
106         self._data = data_trending
107         self._html_layout_file = html_layout_file
108         self._graph_layout_file = graph_layout_file
109         self._tooltip_file = tooltip_file
110
111         # Get structure of tests:
112         tbs = dict()
113         cols = ["job", "test_id", "test_type", "tg_type"]
114         for _, row in self._data[cols].drop_duplicates().iterrows():
115             lst_job = row["job"].split("-")
116             dut = lst_job[1]
117             tbed = "-".join(lst_job[-2:])
118             lst_test = row["test_id"].split(".")
119             if dut == "dpdk":
120                 area = "dpdk"
121             else:
122                 area = ".".join(lst_test[3:-2])
123             suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\
124                 replace("2n-", "")
125             test = lst_test[-1]
126             nic = suite.split("-")[0]
127             for drv in C.DRIVERS:
128                 if drv in test:
129                     if drv == "af-xdp":
130                         driver = "af_xdp"
131                     else:
132                         driver = drv
133                     test = test.replace(f"{drv}-", "")
134                     break
135             else:
136                 driver = "dpdk"
137             infra = "-".join((tbed, nic, driver))
138             lst_test = test.split("-")
139             framesize = lst_test[0]
140             core = lst_test[1] if lst_test[1] else "8C"
141             test = "-".join(lst_test[2: -1])
142
143             if tbs.get(dut, None) is None:
144                 tbs[dut] = dict()
145             if tbs[dut].get(infra, None) is None:
146                 tbs[dut][infra] = dict()
147             if tbs[dut][infra].get(area, None) is None:
148                 tbs[dut][infra][area] = dict()
149             if tbs[dut][infra][area].get(test, None) is None:
150                 tbs[dut][infra][area][test] = dict()
151                 tbs[dut][infra][area][test]["core"] = list()
152                 tbs[dut][infra][area][test]["frame-size"] = list()
153                 tbs[dut][infra][area][test]["test-type"] = list()
154             if core.upper() not in tbs[dut][infra][area][test]["core"]:
155                 tbs[dut][infra][area][test]["core"].append(core.upper())
156             if framesize.upper() not in \
157                     tbs[dut][infra][area][test]["frame-size"]:
158                 tbs[dut][infra][area][test]["frame-size"].append(
159                     framesize.upper()
160                 )
161             if row["test_type"] == "mrr":
162                 if "MRR" not in tbs[dut][infra][area][test]["test-type"]:
163                     tbs[dut][infra][area][test]["test-type"].append("MRR")
164             elif row["test_type"] == "ndrpdr":
165                 if "NDR" not in tbs[dut][infra][area][test]["test-type"]:
166                     tbs[dut][infra][area][test]["test-type"].extend(
167                         ("NDR", "PDR")
168                     )
169             elif row["test_type"] == "hoststack":
170                 if row["tg_type"] in ("iperf", "vpp"):
171                     if "BPS" not in tbs[dut][infra][area][test]["test-type"]:
172                         tbs[dut][infra][area][test]["test-type"].append("BPS")
173                 elif row["tg_type"] == "ab":
174                     if "CPS" not in tbs[dut][infra][area][test]["test-type"]:
175                         tbs[dut][infra][area][test]["test-type"].extend(
176                             ("CPS", "RPS")
177                         )
178         self._spec_tbs = tbs
179
180         # Read from files:
181         self._html_layout = str()
182         self._graph_layout = None
183         self._tooltips = dict()
184
185         try:
186             with open(self._html_layout_file, "r") as file_read:
187                 self._html_layout = file_read.read()
188         except IOError as err:
189             raise RuntimeError(
190                 f"Not possible to open the file {self._html_layout_file}\n{err}"
191             )
192
193         try:
194             with open(self._graph_layout_file, "r") as file_read:
195                 self._graph_layout = load(file_read, Loader=FullLoader)
196         except IOError as err:
197             raise RuntimeError(
198                 f"Not possible to open the file {self._graph_layout_file}\n"
199                 f"{err}"
200             )
201         except YAMLError as err:
202             raise RuntimeError(
203                 f"An error occurred while parsing the specification file "
204                 f"{self._graph_layout_file}\n{err}"
205             )
206
207         try:
208             with open(self._tooltip_file, "r") as file_read:
209                 self._tooltips = load(file_read, Loader=FullLoader)
210         except IOError as err:
211             logging.warning(
212                 f"Not possible to open the file {self._tooltip_file}\n{err}"
213             )
214         except YAMLError as err:
215             logging.warning(
216                 f"An error occurred while parsing the specification file "
217                 f"{self._tooltip_file}\n{err}"
218             )
219
220         # Callbacks:
221         if self._app is not None and hasattr(self, "callbacks"):
222             self.callbacks(self._app)
223
224     @property
225     def html_layout(self):
226         return self._html_layout
227
228     def add_content(self):
229         """Top level method which generated the web page.
230
231         It generates:
232         - Store for user input data,
233         - Navigation bar,
234         - Main area with control panel and ploting area.
235
236         If no HTML layout is provided, an error message is displayed instead.
237
238         :returns: The HTML div with the whole page.
239         :rtype: html.Div
240         """
241
242         if self.html_layout and self._spec_tbs:
243             return html.Div(
244                 id="div-main",
245                 className="small",
246                 children=[
247                     dcc.Store(id="store"),
248                     dcc.Location(id="url", refresh=False),
249                     dbc.Row(
250                         id="row-navbar",
251                         class_name="g-0",
252                         children=[
253                             self._add_navbar()
254                         ]
255                     ),
256                     dbc.Row(
257                         id="row-main",
258                         class_name="g-0",
259                         children=[
260                             self._add_ctrl_col(),
261                             self._add_plotting_col()
262                         ]
263                     ),
264                     dbc.Spinner(
265                         dbc.Offcanvas(
266                             class_name="w-50",
267                             id="offcanvas-metadata",
268                             title="Throughput And Latency",
269                             placement="end",
270                             is_open=False,
271                             children=[
272                                 dbc.Row(id="metadata-tput-lat"),
273                                 dbc.Row(id="metadata-hdrh-graph")
274                             ]
275                         ),
276                         delay_show=C.SPINNER_DELAY
277                     )
278                 ]
279             )
280         else:
281             return html.Div(
282                 dbc.Alert("An Error Occured", color="danger"),
283                 id="div-main-error"
284             )
285
286     def _add_navbar(self):
287         """Add nav element with navigation panel. It is placed on the top.
288
289         :returns: Navigation bar.
290         :rtype: dbc.NavbarSimple
291         """
292         return dbc.NavbarSimple(
293             dbc.NavItem(
294                 dbc.NavLink(
295                     C.TREND_TITLE,
296                     disabled=True,
297                     external_link=True,
298                     href="#"
299                 )
300             ),
301             id="navbarsimple-main",
302             brand=C.BRAND,
303             brand_href="/",
304             brand_external_link=True,
305             class_name="p-2",
306             fluid=True
307         )
308
309     def _add_ctrl_col(self) -> dbc.Col:
310         """Add column with controls. It is placed on the left side.
311
312         :returns: Column with the control panel.
313         :rtype: dbc.Col
314         """
315         return dbc.Col(html.Div(self._add_ctrl_panel(), className="sticky-top"))
316
317     def _add_ctrl_panel(self) -> list:
318         """Add control panel.
319
320         :returns: Control panel.
321         :rtype: list
322         """
323         return [
324             dbc.Row(
325                 dbc.InputGroup(
326                     [
327                         dbc.InputGroupText(
328                             show_tooltip(self._tooltips, "help-dut", "DUT")
329                         ),
330                         dbc.Select(
331                             id={"type": "ctrl-dd", "index": "dut"},
332                             placeholder="Select a Device under Test...",
333                             options=sorted(
334                                 [
335                                     {"label": k, "value": k} \
336                                         for k in self._spec_tbs.keys()
337                                 ],
338                                 key=lambda d: d["label"]
339                             )
340                         )
341                     ],
342                     size="sm"
343                 ),
344                 class_name="g-0 p-1"
345             ),
346             dbc.Row(
347                 dbc.InputGroup(
348                     [
349                         dbc.InputGroupText(
350                             show_tooltip(self._tooltips, "help-infra", "Infra")
351                         ),
352                         dbc.Select(
353                             id={"type": "ctrl-dd", "index": "phy"},
354                             placeholder="Select a Physical Test Bed Topology..."
355                         )
356                     ],
357                     size="sm"
358                 ),
359                 class_name="g-0 p-1"
360             ),
361             dbc.Row(
362                 dbc.InputGroup(
363                     [
364                         dbc.InputGroupText(
365                             show_tooltip(self._tooltips, "help-area", "Area")
366                         ),
367                         dbc.Select(
368                             id={"type": "ctrl-dd", "index": "area"},
369                             placeholder="Select an Area..."
370                         )
371                     ],
372                     size="sm"
373                 ),
374                 class_name="g-0 p-1"
375             ),
376             dbc.Row(
377                 dbc.InputGroup(
378                     [
379                         dbc.InputGroupText(
380                             show_tooltip(self._tooltips, "help-test", "Test")
381                         ),
382                         dbc.Select(
383                             id={"type": "ctrl-dd", "index": "test"},
384                             placeholder="Select a Test..."
385                         )
386                     ],
387                     size="sm"
388                 ),
389                 class_name="g-0 p-1"
390             ),
391             dbc.Row(
392                 dbc.InputGroup(
393                     [
394                         dbc.InputGroupText(show_tooltip(
395                             self._tooltips,
396                             "help-framesize",
397                             "Frame Size"
398                         )),
399                         dbc.Col(
400                             dbc.Checklist(
401                                 id={"type": "ctrl-cl", "index": "frmsize-all"},
402                                 options=C.CL_ALL_DISABLED,
403                                 inline=True,
404                                 class_name="ms-2"
405                             ),
406                             width=2
407                         ),
408                         dbc.Col(
409                             dbc.Checklist(
410                                 id={"type": "ctrl-cl", "index": "frmsize"},
411                                 inline=True
412                             )
413                         )
414                     ],
415                     style={"align-items": "center"},
416                     size="sm"
417                 ),
418                 class_name="g-0 p-1"
419             ),
420             dbc.Row(
421                 dbc.InputGroup(
422                     [
423                         dbc.InputGroupText(show_tooltip(
424                             self._tooltips,
425                             "help-cores",
426                             "Number of Cores"
427                         )),
428                         dbc.Col(
429                             dbc.Checklist(
430                                 id={"type": "ctrl-cl", "index": "core-all"},
431                                 options=C.CL_ALL_DISABLED,
432                                 inline=True,
433                                 class_name="ms-2"
434                             ),
435                             width=2
436                         ),
437                         dbc.Col(
438                             dbc.Checklist(
439                                 id={"type": "ctrl-cl", "index": "core"},
440                                 inline=True
441                             )
442                         )
443                     ],
444                     style={"align-items": "center"},
445                     size="sm"
446                 ),
447                 class_name="g-0 p-1"
448             ),
449             dbc.Row(
450                 dbc.InputGroup(
451                     [
452                         dbc.InputGroupText(show_tooltip(
453                             self._tooltips,
454                             "help-ttype",
455                             "Test Type"
456                         )),
457                         dbc.Col(
458                             dbc.Checklist(
459                                 id={"type": "ctrl-cl", "index": "tsttype-all"},
460                                 options=C.CL_ALL_DISABLED,
461                                 inline=True,
462                                 class_name="ms-2"
463                             ),
464                             width=2
465                         ),
466                         dbc.Col(
467                             dbc.Checklist(
468                                 id={"type": "ctrl-cl", "index": "tsttype"},
469                                 inline=True
470                             )
471                         )
472                     ],
473                     style={"align-items": "center"},
474                     size="sm"
475                 ),
476                 class_name="g-0 p-1"
477             ),
478             dbc.Row(
479                 dbc.InputGroup(
480                     [
481                         dbc.InputGroupText(show_tooltip(
482                             self._tooltips,
483                             "help-normalize",
484                             "Normalization"
485                         )),
486                         dbc.Col(dbc.Checklist(
487                             id="normalize",
488                             options=[{
489                                 "value": "normalize",
490                                 "label": "Normalize to CPU frequency 2GHz"
491                             }],
492                             value=[],
493                             inline=True,
494                             class_name="ms-2"
495                         ))
496                     ],
497                     style={"align-items": "center"},
498                     size="sm"
499                 ),
500                 class_name="g-0 p-1"
501             ),
502             dbc.Row(
503                 dbc.Button(
504                     id={"type": "ctrl-btn", "index": "add-test"},
505                     children="Add Selected",
506                     color="info"
507                 ),
508                 class_name="g-0 p-1"
509             ),
510             dbc.Row(
511                 dbc.ListGroup(
512                     class_name="overflow-auto p-0",
513                     id="lg-selected",
514                     children=[],
515                     style={"max-height": "20em"},
516                     flush=True
517                 ),
518                 id="row-card-sel-tests",
519                 class_name="g-0 p-1",
520                 style=C.STYLE_DISABLED,
521             ),
522             dbc.Row(
523                 dbc.ButtonGroup([
524                     dbc.Button(
525                         "Remove Selected",
526                         id={"type": "ctrl-btn", "index": "rm-test"},
527                         class_name="w-100",
528                         color="info",
529                         disabled=False
530                     ),
531                     dbc.Button(
532                         "Remove All",
533                         id={"type": "ctrl-btn", "index": "rm-test-all"},
534                         class_name="w-100",
535                         color="info",
536                         disabled=False
537                     )
538                 ]),
539                 id="row-btns-sel-tests",
540                 class_name="g-0 p-1",
541                 style=C.STYLE_DISABLED,
542             ),
543             dbc.Stack(
544                 [
545                     dbc.Button(
546                         "Add Telemetry Panel",
547                         id={"type": "telemetry-btn", "index": "open"},
548                         color="info"
549                     ),
550                     dbc.Button("Show URL", id="plot-btn-url", color="info"),
551                     dbc.Modal(
552                         [
553                             dbc.ModalHeader(dbc.ModalTitle("URL")),
554                             dbc.ModalBody(id="mod-url")
555                         ],
556                         id="plot-mod-url",
557                         size="xl",
558                         is_open=False,
559                         scrollable=True
560                     )
561                 ],
562                 id="row-btns-add-tm",
563                 class_name="g-0 p-1",
564                 style=C.STYLE_DISABLED,
565                 gap=2
566             )
567         ]
568
569     def _add_plotting_col(self) -> dbc.Col:
570         """Add column with plots. It is placed on the right side.
571
572         :returns: Column with plots.
573         :rtype: dbc.Col
574         """
575         return dbc.Col(
576             id="col-plotting-area",
577             children=[
578                 dbc.Spinner(
579                     dbc.Row(
580                         id="plotting-area-trending",
581                         class_name="g-0 p-0",
582                         children=C.PLACEHOLDER
583                     ),
584                     delay_show=C.SPINNER_DELAY
585                 ),
586                 dbc.Row(
587                     id="plotting-area-telemetry",
588                     class_name="g-0 p-0",
589                     children=C.PLACEHOLDER
590                 )
591             ],
592             width=9,
593             style=C.STYLE_DISABLED,
594         )
595
596     @staticmethod
597     def _plotting_area_trending(graphs: list) -> dbc.Col:
598         """Generate the plotting area with all its content.
599
600         :param graphs: A list of graphs to be displayed in the trending page.
601         :type graphs: list
602         :returns: A collumn with trending graphs (tput and latency) in tabs.
603         :rtype: dbc.Col
604         """
605         if not graphs:
606             return C.PLACEHOLDER
607
608         if not graphs[0]:
609             return C.PLACEHOLDER
610
611         tab_items = [
612             dbc.Tab(
613                 children=dcc.Graph(
614                     id={"type": "graph", "index": "tput"},
615                     figure=graphs[0]
616                 ),
617                 label="Throughput",
618                 tab_id="tab-tput"
619             )
620         ]
621
622         if graphs[1]:
623             tab_items.append(
624                 dbc.Tab(
625                     children=dcc.Graph(
626                         id={"type": "graph", "index": "lat"},
627                         figure=graphs[1]
628                     ),
629                     label="Latency",
630                     tab_id="tab-lat"
631                 )
632             )
633
634         trending = [
635             dbc.Row(
636                 dbc.Tabs(
637                     children=tab_items,
638                     id="tabs",
639                     active_tab="tab-tput",
640                 ),
641                 class_name="g-0 p-0"
642             ),
643             dbc.Row(
644                 html.Div(
645                     [
646                         dbc.Button(
647                             "Download Data",
648                             id="plot-btn-download",
649                             class_name="me-1",
650                             color="info",
651                             style={"padding": "0rem 1rem"}
652                         ),
653                         dcc.Download(id="download-trending-data")
654                     ],
655                     className="d-grid gap-0 d-md-flex justify-content-md-end"
656                 ),
657                 class_name="g-0 p-0"
658             )
659         ]
660
661         return dbc.Col(
662             children=[
663                 dbc.Accordion(
664                     dbc.AccordionItem(trending, title="Trending"),
665                     class_name="g-0 p-1",
666                     start_collapsed=False,
667                     always_open=True,
668                     active_item=["item-0", ]
669                 ),
670                 dbc.Modal(
671                     [
672                         dbc.ModalHeader(
673                             dbc.ModalTitle("Select a Metric"),
674                             close_button=False
675                         ),
676                         dbc.Spinner(
677                             dbc.ModalBody(Layout._get_telemetry_step_1()),
678                             delay_show=2 * C.SPINNER_DELAY
679                         ),
680                         dbc.ModalFooter([
681                             dbc.Button(
682                                 "Select",
683                                 id={"type": "telemetry-btn", "index": "select"},
684                                 color="success",
685                                 disabled=True
686                             ),
687                             dbc.Button(
688                                 "Cancel",
689                                 id={"type": "telemetry-btn", "index": "cancel"},
690                                 color="info",
691                                 disabled=False
692                             ),
693                             dbc.Button(
694                                 "Remove All",
695                                 id={"type": "telemetry-btn", "index": "rm-all"},
696                                 color="danger",
697                                 disabled=False
698                             )
699                         ])
700                     ],
701                     id={"type": "plot-mod-telemetry", "index": 0},
702                     size="lg",
703                     is_open=False,
704                     scrollable=False,
705                     backdrop="static",
706                     keyboard=False
707                 ),
708                 dbc.Modal(
709                     [
710                         dbc.ModalHeader(
711                             dbc.ModalTitle("Select Labels"),
712                             close_button=False
713                         ),
714                         dbc.Spinner(
715                             dbc.ModalBody(Layout._get_telemetry_step_2()),
716                             delay_show=2 * C.SPINNER_DELAY
717                         ),
718                         dbc.ModalFooter([
719                             dbc.Button(
720                                 "Back",
721                                 id={"type": "telemetry-btn", "index": "back"},
722                                 color="info",
723                                 disabled=False
724                             ),
725                             dbc.Button(
726                                 "Add Telemetry Panel",
727                                 id={"type": "telemetry-btn", "index": "add"},
728                                 color="success",
729                                 disabled=True
730                             ),
731                             dbc.Button(
732                                 "Cancel",
733                                 id={"type": "telemetry-btn", "index": "cancel"},
734                                 color="info",
735                                 disabled=False
736                             )
737                         ])
738                     ],
739                     id={"type": "plot-mod-telemetry", "index": 1},
740                     size="xl",
741                     is_open=False,
742                     scrollable=False,
743                     backdrop="static",
744                     keyboard=False
745                 )
746             ]
747         )
748
749     @staticmethod
750     def _plotting_area_telemetry(graphs: list) -> dbc.Col:
751         """Generate the plotting area with telemetry.
752
753         :param graphs: A list of graphs to be displayed in the telemetry page.
754         :type graphs: list
755         :returns: A collumn with telemetry trending graphs.
756         :rtype: dbc.Col
757         """
758         if not graphs:
759             return C.PLACEHOLDER
760
761         def _plural(iterative):
762             return "s" if len(iterative) > 1 else str()
763
764         panels = list()
765         for idx, graph_set in enumerate(graphs):
766             acc_items = list()
767             for graph in graph_set[0]:
768                 graph_name = ", ".join(graph[1])
769                 acc_items.append(
770                     dbc.AccordionItem(
771                         dcc.Graph(
772                             id={"type": "graph-telemetry", "index": graph_name},
773                             figure=graph[0]
774                         ),
775                         title=(f"Test{_plural(graph[1])}: {graph_name}"),
776                         class_name="g-0 p-0"
777                     )
778                 )
779             panels.append(
780                 dbc.AccordionItem(
781                     [
782                         dbc.Row(
783                             dbc.Accordion(
784                                 children=acc_items,
785                                 class_name="g-0 p-0",
786                                 start_collapsed=True,
787                                 always_open=True,
788                                 flush=True
789                             ),
790                             class_name="g-0 p-0"
791                         ),
792                         dbc.Row(
793                             html.Div(
794                                 [
795                                     dbc.Button(
796                                         "Remove",
797                                         id={
798                                             "type": "tm-btn-remove",
799                                             "index": idx
800                                         },
801                                         class_name="me-1",
802                                         color="danger",
803                                         style={"padding": "0rem 1rem"}
804                                     ),
805                                     dbc.Button(
806                                         "Download Data",
807                                         id={
808                                             "type": "tm-btn-download",
809                                             "index": idx
810                                         },
811                                         class_name="me-1",
812                                         color="info",
813                                         style={"padding": "0rem 1rem"}
814                                     )
815                                 ],
816                             className=\
817                                 "d-grid gap-0 d-md-flex justify-content-md-end"
818                             ),
819                             class_name="g-0 p-0"
820                         )
821                     ],
822                     class_name="g-0 p-0",
823                     title=(
824                         f"Metric{_plural(graph_set[1])}: ",
825                         ", ".join(graph_set[1])
826                     )
827                 )
828             )
829
830         return dbc.Col(
831             dbc.Accordion(
832                 panels,
833                 class_name="g-0 p-1",
834                 start_collapsed=True,
835                 always_open=True
836             )
837         )
838
839     @staticmethod
840     def _get_telemetry_step_1() -> list:
841         """Return the content of the modal window used in the step 1 of metrics
842         selection.
843
844         :returns: A list of dbc rows with 'input' and 'search output'.
845         :rtype: list
846         """
847         return [
848             dbc.Row(
849                 class_name="g-0 p-1",
850                 children=[
851                     dbc.Input(
852                         id={"type": "telemetry-search-in", "index": 0},
853                         placeholder="Start typing a metric name...",
854                         type="text"
855                     )
856                 ]
857             ),
858             dbc.Row(
859                 class_name="g-0 p-1",
860                 children=[
861                     dbc.ListGroup(
862                         class_name="overflow-auto p-0",
863                         id={"type": "telemetry-search-out", "index": 0},
864                         children=[],
865                         style={"max-height": "14em"},
866                         flush=True
867                     )
868                 ]
869             )
870         ]
871
872     @staticmethod
873     def _get_telemetry_step_2() -> list:
874         """Return the content of the modal window used in the step 2 of metrics
875         selection.
876
877         :returns: A list of dbc rows with 'container with dynamic dropdowns' and
878             'search output'.
879         :rtype: list
880         """
881         return [
882             dbc.Row(
883                 "Add content here.",
884                 id={"type": "tm-container", "index": 0},
885                 class_name="g-0 p-1"
886             ),
887             dbc.Row(
888                 [
889                     dbc.Col(
890                         dbc.Checkbox(
891                             id={"type": "cb-all-in-one", "index": 0},
892                             label="All Metrics in one Graph"
893                         ),
894                         width=6
895                     ),
896                     dbc.Col(
897                         dbc.Checkbox(
898                             id={"type": "cb-ignore-host", "index": 0},
899                             label="Ignore Host"
900                         ),
901                         width=6
902                     )
903                 ],
904                 class_name="g-0 p-2"
905             ),
906             dbc.Row(
907                 dbc.Textarea(
908                     id={"type": "tm-list-metrics", "index": 0},
909                     rows=20,
910                     size="sm",
911                     wrap="off",
912                     readonly=True
913                 ),
914                 class_name="g-0 p-1"
915             )
916         ]
917
918     def callbacks(self, app):
919         """Callbacks for the whole application.
920
921         :param app: The application.
922         :type app: Flask
923         """
924
925         @app.callback(
926             Output("store", "data"),
927             Output("plotting-area-trending", "children"),
928             Output("plotting-area-telemetry", "children"),
929             Output("col-plotting-area", "style"),
930             Output("row-card-sel-tests", "style"),
931             Output("row-btns-sel-tests", "style"),
932             Output("row-btns-add-tm", "style"),
933             Output("lg-selected", "children"),
934             Output({"type": "telemetry-search-out", "index": ALL}, "children"),
935             Output({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
936             Output({"type": "telemetry-btn", "index": ALL}, "disabled"),
937             Output({"type": "tm-container", "index": ALL}, "children"),
938             Output({"type": "tm-list-metrics", "index": ALL}, "value"),
939             Output({"type": "ctrl-dd", "index": "dut"}, "value"),
940             Output({"type": "ctrl-dd", "index": "phy"}, "options"),
941             Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
942             Output({"type": "ctrl-dd", "index": "phy"}, "value"),
943             Output({"type": "ctrl-dd", "index": "area"}, "options"),
944             Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
945             Output({"type": "ctrl-dd", "index": "area"}, "value"),
946             Output({"type": "ctrl-dd", "index": "test"}, "options"),
947             Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
948             Output({"type": "ctrl-dd", "index": "test"}, "value"),
949             Output({"type": "ctrl-cl", "index": "core"}, "options"),
950             Output({"type": "ctrl-cl", "index": "core"}, "value"),
951             Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
952             Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
953             Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
954             Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
955             Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
956             Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
957             Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
958             Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
959             Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
960             Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
961             Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
962             Output("normalize", "value"),
963
964             State("store", "data"),
965             State({"type": "sel-cl", "index": ALL}, "value"),
966             State({"type": "cb-all-in-one", "index": ALL}, "value"),
967             State({"type": "cb-ignore-host", "index": ALL}, "value"),
968             State({"type": "telemetry-search-out", "index": ALL}, "children"),
969             State({"type": "plot-mod-telemetry", "index": ALL}, "is_open"),
970             State({"type": "telemetry-btn", "index": ALL}, "disabled"),
971             State({"type": "tm-container", "index": ALL}, "children"),
972             State({"type": "tm-list-metrics", "index": ALL}, "value"),
973             State({"type": "tele-cl", "index": ALL}, "value"),
974
975             Input("url", "href"),
976             Input({"type": "tm-dd", "index": ALL}, "value"),
977
978             Input("normalize", "value"),
979             Input({"type": "telemetry-search-in", "index": ALL}, "value"),
980             Input({"type": "telemetry-btn", "index": ALL}, "n_clicks"),
981             Input({"type": "tm-btn-remove", "index": ALL}, "n_clicks"),
982             Input({"type": "ctrl-dd", "index": ALL}, "value"),
983             Input({"type": "ctrl-cl", "index": ALL}, "value"),
984             Input({"type": "ctrl-btn", "index": ALL}, "n_clicks"),
985
986             prevent_initial_call=True
987         )
988         def _update_application(
989                 store: dict,
990                 lst_sel: list,
991                 all_in_one: list,
992                 ignore_host: list,
993                 search_out: list,
994                 is_open: list,
995                 tm_btns_disabled: list,
996                 tm_dd: list,
997                 list_metrics: list,
998                 cl_metrics: list,
999                 href: str,
1000                 tm_dd_in: list,
1001                 *_
1002             ) -> tuple:
1003             """Update the application when the event is detected.
1004             """
1005
1006             if store is None:
1007                 store = {
1008                     "control-panel": dict(),
1009                     "selected-tests": list(),
1010                     "trending-graphs": None,
1011                     "telemetry-data": dict(),
1012                     "selected-metrics": dict(),
1013                     "telemetry-panels": list(),
1014                     "telemetry-all-in-one": list(),
1015                     "telemetry-ignore-host": list(),
1016                     "telemetry-graphs": list(),
1017                     "url": str()
1018                 }
1019
1020             ctrl_panel = ControlPanel(
1021                 CP_PARAMS,
1022                 store.get("control-panel", dict())
1023             )
1024             store_sel = store["selected-tests"]
1025             tm_data = store["telemetry-data"]
1026             tm_user = store["selected-metrics"]
1027             tm_panels = store["telemetry-panels"]
1028             tm_all_in_one = store["telemetry-all-in-one"]
1029             tm_ignore_host = store["telemetry-ignore-host"]
1030
1031             plotting_area_telemetry = no_update
1032             on_draw = [False, False]  # 0 --> trending, 1 --> telemetry
1033
1034             # Parse the url:
1035             parsed_url = url_decode(href)
1036             if parsed_url:
1037                 url_params = parsed_url["params"]
1038             else:
1039                 url_params = None
1040
1041             if tm_user is None:
1042                 # Telemetry user data
1043                 # The data provided by user or result of user action
1044                 tm_user = {
1045                     # List of unique metrics:
1046                     "unique_metrics": list(),
1047                     # List of metrics selected by user:
1048                     "selected_metrics": list(),
1049                     # Labels from metrics selected by user (key: label name,
1050                     # value: list of all possible values):
1051                     "unique_labels": dict(),
1052                     # Labels selected by the user (subset of 'unique_labels'):
1053                     "selected_labels": dict(),
1054                     # All unique metrics with labels (output from the step 1)
1055                     # converted from pandas dataframe to dictionary.
1056                     "unique_metrics_with_labels": dict(),
1057                     # Metrics with labels selected by the user using dropdowns.
1058                     "selected_metrics_with_labels": dict()
1059                 }
1060             tm = TelemetryData(store_sel) if store_sel else TelemetryData()
1061
1062             trigger = Trigger(callback_context.triggered)
1063             if trigger.type == "url" and url_params:
1064                 telemetry = None
1065                 try:
1066                     store_sel = literal_eval(url_params["store_sel"][0])
1067                     normalize = literal_eval(url_params["norm"][0])
1068                     telemetry = literal_eval(url_params["telemetry"][0])
1069                     url_p = url_params.get("all-in-one", ["[[None]]"])
1070                     tm_all_in_one = literal_eval(url_p[0])
1071                     url_p = url_params.get("ignore-host", ["[[None]]"])
1072                     tm_ignore_host = literal_eval(url_p[0])
1073                     if not isinstance(telemetry, list):
1074                         telemetry = [telemetry, ]
1075                 except (KeyError, IndexError, AttributeError, ValueError):
1076                     pass
1077                 if store_sel:
1078                     last_test = store_sel[-1]
1079                     test = self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1080                         [last_test["area"]][last_test["test"]]
1081                     ctrl_panel.set({
1082                         "dd-dut-val": last_test["dut"],
1083                         "dd-phy-val": last_test["phy"],
1084                         "dd-phy-opt": generate_options(
1085                             self._spec_tbs[last_test["dut"]].keys()
1086                         ),
1087                         "dd-phy-dis": False,
1088                         "dd-area-val": last_test["area"],
1089                         "dd-area-opt": [
1090                             {"label": label(v), "value": v} for v in sorted(
1091                                 self._spec_tbs[last_test["dut"]]\
1092                                     [last_test["phy"]].keys()
1093                             )
1094                         ],
1095                         "dd-area-dis": False,
1096                         "dd-test-val": last_test["test"],
1097                         "dd-test-opt": generate_options(
1098                             self._spec_tbs[last_test["dut"]][last_test["phy"]]\
1099                                 [last_test["area"]].keys()
1100                         ),
1101                         "dd-test-dis": False,
1102                         "cl-core-opt": generate_options(test["core"]),
1103                         "cl-core-val": [last_test["core"].upper(), ],
1104                         "cl-core-all-val": list(),
1105                         "cl-core-all-opt": C.CL_ALL_ENABLED,
1106                         "cl-frmsize-opt": generate_options(test["frame-size"]),
1107                         "cl-frmsize-val": [last_test["framesize"].upper(), ],
1108                         "cl-frmsize-all-val": list(),
1109                         "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1110                         "cl-tsttype-opt": generate_options(test["test-type"]),
1111                         "cl-tsttype-val": [last_test["testtype"].upper(), ],
1112                         "cl-tsttype-all-val": list(),
1113                         "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1114                         "cl-normalize-val": normalize,
1115                         "btn-add-dis": False
1116                     })
1117                     store["trending-graphs"] = None
1118                     store["telemetry-graphs"] = list()
1119                     on_draw[0] = True
1120                     if telemetry:
1121                         tm = TelemetryData(store_sel)
1122                         tm.from_dataframe(self._data)
1123                         tm_data = tm.to_json()
1124                         tm.from_json(tm_data)
1125                         tm_panels = telemetry
1126                         on_draw[1] = True
1127             elif trigger.type == "normalize":
1128                 ctrl_panel.set({"cl-normalize-val": trigger.value})
1129                 store["trending-graphs"] = None
1130                 on_draw[0] = True
1131             elif trigger.type == "ctrl-dd":
1132                 if trigger.idx == "dut":
1133                     try:
1134                         options = generate_options(
1135                             self._spec_tbs[trigger.value].keys()
1136                         )
1137                         disabled = False
1138                     except KeyError:
1139                         options = list()
1140                         disabled = True
1141                     ctrl_panel.set({
1142                         "dd-dut-val": trigger.value,
1143                         "dd-phy-val": str(),
1144                         "dd-phy-opt": options,
1145                         "dd-phy-dis": disabled,
1146                         "dd-area-val": str(),
1147                         "dd-area-opt": list(),
1148                         "dd-area-dis": True,
1149                         "dd-test-val": str(),
1150                         "dd-test-opt": list(),
1151                         "dd-test-dis": True,
1152                         "cl-core-opt": list(),
1153                         "cl-core-val": list(),
1154                         "cl-core-all-val": list(),
1155                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1156                         "cl-frmsize-opt": list(),
1157                         "cl-frmsize-val": list(),
1158                         "cl-frmsize-all-val": list(),
1159                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1160                         "cl-tsttype-opt": list(),
1161                         "cl-tsttype-val": list(),
1162                         "cl-tsttype-all-val": list(),
1163                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1164                         "btn-add-dis": True
1165                     })
1166                 elif trigger.idx == "phy":
1167                     try:
1168                         dut = ctrl_panel.get("dd-dut-val")
1169                         phy = self._spec_tbs[dut][trigger.value]
1170                         options = [{"label": label(v), "value": v} \
1171                             for v in sorted(phy.keys())]
1172                         disabled = False
1173                     except KeyError:
1174                         options = list()
1175                         disabled = True
1176                     ctrl_panel.set({
1177                         "dd-phy-val": trigger.value,
1178                         "dd-area-val": str(),
1179                         "dd-area-opt": options,
1180                         "dd-area-dis": disabled,
1181                         "dd-test-val": str(),
1182                         "dd-test-opt": list(),
1183                         "dd-test-dis": True,
1184                         "cl-core-opt": list(),
1185                         "cl-core-val": list(),
1186                         "cl-core-all-val": list(),
1187                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1188                         "cl-frmsize-opt": list(),
1189                         "cl-frmsize-val": list(),
1190                         "cl-frmsize-all-val": list(),
1191                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1192                         "cl-tsttype-opt": list(),
1193                         "cl-tsttype-val": list(),
1194                         "cl-tsttype-all-val": list(),
1195                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1196                         "btn-add-dis": True
1197                     })  
1198                 elif trigger.idx == "area":
1199                     try:
1200                         dut = ctrl_panel.get("dd-dut-val")
1201                         phy = ctrl_panel.get("dd-phy-val")
1202                         area = self._spec_tbs[dut][phy][trigger.value]
1203                         options = generate_options(area.keys())
1204                         disabled = False
1205                     except KeyError:
1206                         options = list()
1207                         disabled = True
1208                     ctrl_panel.set({
1209                         "dd-area-val": trigger.value,
1210                         "dd-test-val": str(),
1211                         "dd-test-opt": options,
1212                         "dd-test-dis": disabled,
1213                         "cl-core-opt": list(),
1214                         "cl-core-val": list(),
1215                         "cl-core-all-val": list(),
1216                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1217                         "cl-frmsize-opt": list(),
1218                         "cl-frmsize-val": list(),
1219                         "cl-frmsize-all-val": list(),
1220                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1221                         "cl-tsttype-opt": list(),
1222                         "cl-tsttype-val": list(),
1223                         "cl-tsttype-all-val": list(),
1224                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1225                         "btn-add-dis": True
1226                     })
1227                 elif trigger.idx == "test":
1228                     dut = ctrl_panel.get("dd-dut-val")
1229                     phy = ctrl_panel.get("dd-phy-val")
1230                     area = ctrl_panel.get("dd-area-val")
1231                     if all((dut, phy, area, trigger.value, )):
1232                         test = self._spec_tbs[dut][phy][area][trigger.value]
1233                         ctrl_panel.set({
1234                             "dd-test-val": trigger.value,
1235                             "cl-core-opt": generate_options(test["core"]),
1236                             "cl-core-val": list(),
1237                             "cl-core-all-val": list(),
1238                             "cl-core-all-opt": C.CL_ALL_ENABLED,
1239                             "cl-frmsize-opt": \
1240                                 generate_options(test["frame-size"]),
1241                             "cl-frmsize-val": list(),
1242                             "cl-frmsize-all-val": list(),
1243                             "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1244                             "cl-tsttype-opt": \
1245                                 generate_options(test["test-type"]),
1246                             "cl-tsttype-val": list(),
1247                             "cl-tsttype-all-val": list(),
1248                             "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1249                             "btn-add-dis": True
1250                         })
1251             elif trigger.type == "ctrl-cl":
1252                 param = trigger.idx.split("-")[0]
1253                 if "-all" in trigger.idx:
1254                     c_sel, c_all, c_id = list(), trigger.value, "all"
1255                 else:
1256                     c_sel, c_all, c_id = trigger.value, list(), str()
1257                 val_sel, val_all = sync_checklists(
1258                     options=ctrl_panel.get(f"cl-{param}-opt"),
1259                     sel=c_sel,
1260                     all=c_all,
1261                     id=c_id
1262                 )
1263                 ctrl_panel.set({
1264                     f"cl-{param}-val": val_sel,
1265                     f"cl-{param}-all-val": val_all,
1266                 })
1267                 if all((ctrl_panel.get("cl-core-val"),
1268                         ctrl_panel.get("cl-frmsize-val"),
1269                         ctrl_panel.get("cl-tsttype-val"), )):
1270                     ctrl_panel.set({"btn-add-dis": False})
1271                 else:
1272                     ctrl_panel.set({"btn-add-dis": True})
1273             elif trigger.type == "ctrl-btn":
1274                 tm_panels = list()
1275                 tm_all_in_one = list()
1276                 tm_ignore_host = list()
1277                 store["trending-graphs"] = None
1278                 store["telemetry-graphs"] = list()
1279                 # on_draw[0] = True
1280                 on_draw = [True, True]
1281                 if trigger.idx == "add-test":
1282                     dut = ctrl_panel.get("dd-dut-val")
1283                     phy = ctrl_panel.get("dd-phy-val")
1284                     area = ctrl_panel.get("dd-area-val")
1285                     test = ctrl_panel.get("dd-test-val")
1286                     # Add selected test(s) to the list of tests in store:
1287                     if store_sel is None:
1288                         store_sel = list()
1289                     for core in ctrl_panel.get("cl-core-val"):
1290                         for framesize in ctrl_panel.get("cl-frmsize-val"):
1291                             for ttype in ctrl_panel.get("cl-tsttype-val"):
1292                                 if dut == "trex":
1293                                     core = str()
1294                                 tid = "-".join((
1295                                     dut,
1296                                     phy.replace('af_xdp', 'af-xdp'),
1297                                     area,
1298                                     framesize.lower(),
1299                                     core.lower(),
1300                                     test,
1301                                     ttype.lower()
1302                                 ))
1303                                 if tid not in [i["id"] for i in store_sel]:
1304                                     store_sel.append({
1305                                         "id": tid,
1306                                         "dut": dut,
1307                                         "phy": phy,
1308                                         "area": area,
1309                                         "test": test,
1310                                         "framesize": framesize.lower(),
1311                                         "core": core.lower(),
1312                                         "testtype": ttype.lower()
1313                                     })
1314                     store_sel = sorted(store_sel, key=lambda d: d["id"])
1315                     if C.CLEAR_ALL_INPUTS:
1316                         ctrl_panel.set(ctrl_panel.defaults)
1317                 elif trigger.idx == "rm-test" and lst_sel:
1318                     new_store_sel = list()
1319                     for idx, item in enumerate(store_sel):
1320                         if not lst_sel[idx]:
1321                             new_store_sel.append(item)
1322                     store_sel = new_store_sel
1323                 elif trigger.idx == "rm-test-all":
1324                     store_sel = list()
1325             elif trigger.type == "telemetry-btn":
1326                 if trigger.idx in ("open", "back"):
1327                     tm.from_dataframe(self._data)
1328                     tm_data = tm.to_json()
1329                     tm_user["unique_metrics"] = tm.unique_metrics
1330                     tm_user["selected_metrics"] = list()
1331                     tm_user["unique_labels"] = dict()
1332                     tm_user["selected_labels"] = dict()
1333                     search_out = (
1334                         get_list_group_items(tm_user["unique_metrics"],
1335                             "tele-cl", False),
1336                     )
1337                     is_open = (True, False)
1338                     tm_btns_disabled[1], tm_btns_disabled[5] = False, True
1339                 elif trigger.idx == "select":
1340                     if any(cl_metrics):
1341                         tm.from_json(tm_data)
1342                         if not tm_user["selected_metrics"]:
1343                             tm_user["selected_metrics"] = \
1344                                 tm_user["unique_metrics"]
1345                         metrics = [a for a, b in \
1346                             zip(tm_user["selected_metrics"], cl_metrics) if b]
1347                         tm_user["selected_metrics"] = metrics
1348                         tm_user["unique_labels"] = \
1349                             tm.get_selected_labels(metrics)
1350                         tm_user["unique_metrics_with_labels"] = \
1351                             tm.unique_metrics_with_labels
1352                         list_metrics[0] = tm.str_metrics
1353                         tm_dd[0] = _get_dd_container(tm_user["unique_labels"])
1354                         if list_metrics[0]:
1355                             tm_btns_disabled[1] = True
1356                             tm_btns_disabled[4] = False
1357                         is_open = (False, True)
1358                     else:
1359                         is_open = (True, False)
1360                 elif trigger.idx == "add":
1361                     tm.from_json(tm_data)
1362                     tm_panels.append(tm_user["selected_metrics_with_labels"])
1363                     tm_all_in_one.append(all_in_one)
1364                     tm_ignore_host.append(ignore_host)
1365                     is_open = (False, False)
1366                     tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1367                     on_draw = [True, True]
1368                 elif trigger.idx == "cancel":
1369                     is_open = (False, False)
1370                     tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1371                 elif trigger.idx == "rm-all":
1372                     tm_panels = list()
1373                     tm_all_in_one = list()
1374                     tm_ignore_host = list()
1375                     tm_user = None
1376                     is_open = (False, False)
1377                     tm_btns_disabled[1], tm_btns_disabled[5] = True, True
1378                     plotting_area_telemetry = C.PLACEHOLDER
1379             elif trigger.type == "telemetry-search-in":
1380                 tm.from_metrics(tm_user["unique_metrics"])
1381                 tm_user["selected_metrics"] = \
1382                     tm.search_unique_metrics(trigger.value)
1383                 search_out = (get_list_group_items(
1384                     tm_user["selected_metrics"],
1385                     type="tele-cl",
1386                     colorize=False
1387                 ), )
1388                 is_open = (True, False)
1389             elif trigger.type == "tm-dd":
1390                 tm.from_metrics_with_labels(
1391                     tm_user["unique_metrics_with_labels"]
1392                 )
1393                 selected = dict()
1394                 previous_itm = None
1395                 for itm in tm_dd_in:
1396                     if itm is None:
1397                         show_new = True
1398                     elif isinstance(itm, str):
1399                         show_new = False
1400                         selected[itm] = list()
1401                     elif isinstance(itm, list):
1402                         if previous_itm is not None:
1403                             selected[previous_itm] = itm
1404                         show_new = True
1405                     previous_itm = itm
1406                 tm_dd[0] = _get_dd_container(
1407                     tm_user["unique_labels"],
1408                     selected,
1409                     show_new
1410                 )
1411                 sel_metrics = tm.filter_selected_metrics_by_labels(selected)
1412                 tm_user["selected_metrics_with_labels"] = sel_metrics.to_dict()
1413                 if not sel_metrics.empty:
1414                     list_metrics[0] = tm.metrics_to_str(sel_metrics)
1415                     tm_btns_disabled[5] = False
1416                 else:
1417                     list_metrics[0] = str()
1418             elif trigger.type == "tm-btn-remove":
1419                 del tm_panels[trigger.idx]
1420                 del tm_all_in_one[trigger.idx]
1421                 del tm_ignore_host[trigger.idx]
1422                 del store["telemetry-graphs"][trigger.idx]
1423                 tm.from_json(tm_data)
1424                 on_draw = [True, True]
1425
1426             new_url_params = {
1427                 "store_sel": store_sel,
1428                 "norm": ctrl_panel.get("cl-normalize-val")
1429             }
1430             if tm_panels:
1431                 new_url_params["telemetry"] = tm_panels
1432                 new_url_params["all-in-one"] = tm_all_in_one
1433                 new_url_params["ignore-host"] = tm_ignore_host
1434
1435             if on_draw[0]:  # Trending
1436                 if store_sel:
1437                     lg_selected = get_list_group_items(store_sel, "sel-cl")
1438                     if store["trending-graphs"]:
1439                         graphs = store["trending-graphs"]
1440                     else:
1441                         graphs = graph_trending(
1442                             self._data,
1443                             store_sel,
1444                             self._graph_layout,
1445                             bool(ctrl_panel.get("cl-normalize-val"))
1446                         )
1447                         if graphs and graphs[0]:
1448                             store["trending-graphs"] = graphs
1449                     plotting_area_trending = \
1450                         Layout._plotting_area_trending(graphs)
1451
1452                     # Telemetry
1453                     start_idx = len(store["telemetry-graphs"])
1454                     end_idx = len(tm_panels)
1455                     if not end_idx:
1456                         plotting_area_telemetry = C.PLACEHOLDER
1457                     elif on_draw[1] and (end_idx >= start_idx):
1458                         for idx in range(start_idx, end_idx):
1459                             store["telemetry-graphs"].append(graph_tm_trending(
1460                                 tm.select_tm_trending_data(
1461                                     tm_panels[idx],
1462                                     ignore_host=bool(tm_ignore_host[idx][0])
1463                                 ),
1464                                 self._graph_layout,
1465                                 bool(tm_all_in_one[idx][0])
1466                             ))
1467                         plotting_area_telemetry = \
1468                             Layout._plotting_area_telemetry(
1469                                 store["telemetry-graphs"]
1470                             )
1471                     col_plotting_area = C.STYLE_ENABLED
1472                     row_card_sel_tests = C.STYLE_ENABLED
1473                     row_btns_sel_tests = C.STYLE_ENABLED
1474                     row_btns_add_tm = C.STYLE_ENABLED
1475                 else:
1476                     plotting_area_trending = no_update
1477                     plotting_area_telemetry = C.PLACEHOLDER
1478                     col_plotting_area = C.STYLE_DISABLED
1479                     row_card_sel_tests = C.STYLE_DISABLED
1480                     row_btns_sel_tests = C.STYLE_DISABLED
1481                     row_btns_add_tm = C.STYLE_DISABLED
1482                     lg_selected = no_update
1483                     store_sel = list()
1484                     tm_panels = list()
1485                     tm_all_in_one = list()
1486                     tm_ignore_host = list()
1487                     tm_user = None
1488             else:
1489                 plotting_area_trending = no_update
1490                 col_plotting_area = no_update
1491                 row_card_sel_tests = no_update
1492                 row_btns_sel_tests = no_update
1493                 row_btns_add_tm = no_update
1494                 lg_selected = no_update
1495
1496             store["url"] = gen_new_url(parsed_url, new_url_params)
1497             store["control-panel"] = ctrl_panel.panel
1498             store["selected-tests"] = store_sel
1499             store["telemetry-data"] = tm_data
1500             store["selected-metrics"] = tm_user
1501             store["telemetry-panels"] = tm_panels
1502             store["telemetry-all-in-one"] = tm_all_in_one
1503             store["telemetry-ignore-host"] = tm_ignore_host
1504             ret_val = [
1505                 store,
1506                 plotting_area_trending,
1507                 plotting_area_telemetry,
1508                 col_plotting_area,
1509                 row_card_sel_tests,
1510                 row_btns_sel_tests,
1511                 row_btns_add_tm,
1512                 lg_selected,
1513                 search_out,
1514                 is_open,
1515                 tm_btns_disabled,
1516                 tm_dd,
1517                 list_metrics
1518             ]
1519             ret_val.extend(ctrl_panel.values)
1520             return ret_val
1521
1522         @app.callback(
1523             Output("plot-mod-url", "is_open"),
1524             Output("mod-url", "children"),
1525             State("store", "data"),
1526             State("plot-mod-url", "is_open"),
1527             Input("plot-btn-url", "n_clicks")
1528         )
1529         def toggle_plot_mod_url(store, is_open, n_clicks):
1530             """Toggle the modal window with url.
1531             """
1532             if not store:
1533                 raise PreventUpdate
1534
1535             if n_clicks:
1536                 return not is_open, store.get("url", str())
1537             return is_open, store["url"]
1538
1539         def _get_dd_container(
1540                 all_labels: dict,
1541                 selected_labels: dict=dict(),
1542                 show_new=True
1543             ) -> list:
1544             """Generate a container with dropdown selection boxes depenting on
1545             the input data.
1546
1547             :param all_labels: A dictionary with unique labels and their
1548                 possible values.
1549             :param selected_labels: A dictionalry with user selected lables and
1550                 their values.
1551             :param show_new: If True, a dropdown selection box to add a new
1552                 label is displayed.
1553             :type all_labels: dict
1554             :type selected_labels: dict
1555             :type show_new: bool
1556             :returns: A list of dbc rows with dropdown selection boxes.
1557             :rtype: list
1558             """
1559
1560             def _row(
1561                     id: str,
1562                     lopts: list=list(),
1563                     lval: str=str(),
1564                     vopts: list=list(),
1565                     vvals: list=list()
1566                 ) -> dbc.Row:
1567                 """Generates a dbc row with dropdown boxes.
1568
1569                 :param id: A string added to the dropdown ID.
1570                 :param lopts: A list of options for 'label' dropdown.
1571                 :param lval: Value of 'label' dropdown.
1572                 :param vopts: A list of options for 'value' dropdown.
1573                 :param vvals: A list of values for 'value' dropdown.
1574                 :type id: str
1575                 :type lopts: list
1576                 :type lval: str
1577                 :type vopts: list
1578                 :type vvals: list
1579                 :returns: dbc row with dropdown boxes.
1580                 :rtype: dbc.Row
1581                 """
1582                 children = list()
1583                 if lopts:
1584                     children.append(
1585                         dbc.Col(
1586                             width=6,
1587                             children=[
1588                                 dcc.Dropdown(
1589                                     id={
1590                                         "type": "tm-dd",
1591                                         "index": f"label-{id}"
1592                                     },
1593                                     placeholder="Select a label...",
1594                                     optionHeight=20,
1595                                     multi=False,
1596                                     options=lopts,
1597                                     value=lval if lval else None
1598                                 )
1599                             ]
1600                         )
1601                     )
1602                     if vopts:
1603                         children.append(
1604                             dbc.Col(
1605                                 width=6,
1606                                 children=[
1607                                     dcc.Dropdown(
1608                                         id={
1609                                             "type": "tm-dd",
1610                                             "index": f"value-{id}"
1611                                         },
1612                                         placeholder="Select a value...",
1613                                         optionHeight=20,
1614                                         multi=True,
1615                                         options=vopts,
1616                                         value=vvals if vvals else None
1617                                     )
1618                                 ]
1619                             )
1620                         )
1621
1622                 return dbc.Row(class_name="g-0 p-1", children=children)
1623
1624             container = list()
1625
1626             # Display rows with items in 'selected_labels'; label on the left,
1627             # values on the right:
1628             keys_left = list(all_labels.keys())
1629             for idx, label in enumerate(selected_labels.keys()):
1630                 container.append(_row(
1631                     id=idx,
1632                     lopts=deepcopy(keys_left),
1633                     lval=label,
1634                     vopts=all_labels[label],
1635                     vvals=selected_labels[label]
1636                 ))
1637                 keys_left.remove(label)
1638
1639             # Display row with dd with labels on the left, right side is empty:
1640             if show_new and keys_left:
1641                 container.append(_row(id="new", lopts=keys_left))
1642
1643             return container
1644
1645         @app.callback(
1646             Output("metadata-tput-lat", "children"),
1647             Output("metadata-hdrh-graph", "children"),
1648             Output("offcanvas-metadata", "is_open"),
1649             Input({"type": "graph", "index": ALL}, "clickData"),
1650             prevent_initial_call=True
1651         )
1652         def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1653             """Generates the data for the offcanvas displayed when a particular
1654             point in a graph is clicked on.
1655
1656             :param graph_data: The data from the clicked point in the graph.
1657             :type graph_data: dict
1658             :returns: The data to be displayed on the offcanvas and the
1659                 information to show the offcanvas.
1660             :rtype: tuple(list, list, bool)
1661             """
1662
1663             trigger = Trigger(callback_context.triggered)
1664
1665             try:
1666                 idx = 0 if trigger.idx == "tput" else 1
1667                 graph_data = graph_data[idx]["points"][0]
1668             except (IndexError, KeyError, ValueError, TypeError):
1669                 raise PreventUpdate
1670
1671             metadata = no_update
1672             graph = list()
1673
1674             children = [
1675                 dbc.ListGroupItem(
1676                     [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1677                 ) for x in graph_data.get("text", "").split("<br>")
1678             ]
1679             if trigger.idx == "tput":
1680                 title = "Throughput"
1681             elif trigger.idx == "lat":
1682                 title = "Latency"
1683                 hdrh_data = graph_data.get("customdata", None)
1684                 if hdrh_data:
1685                     graph = [dbc.Card(
1686                         class_name="gy-2 p-0",
1687                         children=[
1688                             dbc.CardHeader(hdrh_data.pop("name")),
1689                             dbc.CardBody(children=[
1690                                 dcc.Graph(
1691                                     id="hdrh-latency-graph",
1692                                     figure=graph_hdrh_latency(
1693                                         hdrh_data, self._graph_layout
1694                                     )
1695                                 )
1696                             ])
1697                         ])
1698                     ]
1699             else:
1700                 raise PreventUpdate
1701
1702             metadata = [
1703                 dbc.Card(
1704                     class_name="gy-2 p-0",
1705                     children=[
1706                         dbc.CardHeader(children=[
1707                             dcc.Clipboard(
1708                                 target_id="tput-lat-metadata",
1709                                 title="Copy",
1710                                 style={"display": "inline-block"}
1711                             ),
1712                             title
1713                         ]),
1714                         dbc.CardBody(
1715                             id="tput-lat-metadata",
1716                             class_name="p-0",
1717                             children=[dbc.ListGroup(children, flush=True), ]
1718                         )
1719                     ]
1720                 )
1721             ]
1722
1723             return metadata, graph, True
1724
1725         @app.callback(
1726             Output("download-trending-data", "data"),
1727             State("store", "data"),
1728             Input("plot-btn-download", "n_clicks"),
1729             Input({"type": "tm-btn-download", "index": ALL}, "n_clicks"),
1730             prevent_initial_call=True
1731         )
1732         def _download_data(store: list, *_) -> dict:
1733             """Download the data
1734
1735             :param store_sel: List of tests selected by user stored in the
1736                 browser.
1737             :type store_sel: list
1738             :returns: dict of data frame content (base64 encoded) and meta data
1739                 used by the Download component.
1740             :rtype: dict
1741             """
1742
1743             if not store:
1744                 raise PreventUpdate
1745             if not store["selected-tests"]:
1746                 raise PreventUpdate
1747             
1748             df = pd.DataFrame()
1749             
1750             trigger = Trigger(callback_context.triggered)
1751             if not trigger.value:
1752                 raise PreventUpdate
1753             
1754             if trigger.type == "plot-btn-download":
1755                 data = list()
1756                 for itm in store["selected-tests"]:
1757                     sel_data = select_trending_data(self._data, itm)
1758                     if sel_data is None:
1759                         continue
1760                     data.append(sel_data)
1761                 df = pd.concat(data, ignore_index=True, copy=False)
1762                 file_name = C.TREND_DOWNLOAD_FILE_NAME
1763             elif trigger.type == "tm-btn-download":
1764                 tm = TelemetryData(store["selected-tests"])
1765                 tm.from_json(store["telemetry-data"])
1766                 df = tm.select_tm_trending_data(
1767                     store["telemetry-panels"][trigger.idx]
1768                 )
1769                 file_name = C.TELEMETRY_DOWNLOAD_FILE_NAME
1770             else:
1771                 raise PreventUpdate
1772
1773             return dcc.send_data_frame(df.to_csv, file_name)