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