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