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