C-Dash: Prepare layout for telemetry in trending
[csit.git] / csit.infra.dash / app / cdash / trending / layout.py
1 # Copyright (c) 2022 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
31 from ..utils.constants import Constants as C
32 from ..utils.control_panel import ControlPanel
33 from ..utils.trigger import Trigger
34 from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
35     generate_options, get_list_group_items
36 from ..utils.url_processing import url_decode
37 from ..data.data import Data
38 from .graphs import graph_trending, graph_hdrh_latency, select_trending_data
39
40
41 # Control panel partameters and their default values.
42 CP_PARAMS = {
43     "dd-dut-val": str(),
44     "dd-phy-opt": list(),
45     "dd-phy-dis": True,
46     "dd-phy-val": str(),
47     "dd-area-opt": list(),
48     "dd-area-dis": True,
49     "dd-area-val": str(),
50     "dd-test-opt": list(),
51     "dd-test-dis": True,
52     "dd-test-val": str(),
53     "cl-core-opt": list(),
54     "cl-core-val": list(),
55     "cl-core-all-val": list(),
56     "cl-core-all-opt": C.CL_ALL_DISABLED,
57     "cl-frmsize-opt": list(),
58     "cl-frmsize-val": list(),
59     "cl-frmsize-all-val": list(),
60     "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
61     "cl-tsttype-opt": list(),
62     "cl-tsttype-val": list(),
63     "cl-tsttype-all-val": list(),
64     "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
65     "btn-add-dis": True,
66     "cl-normalize-val": list()
67 }
68
69
70 class Layout:
71     """The layout of the dash app and the callbacks.
72     """
73
74     def __init__(self, app: Flask, html_layout_file: str,
75         graph_layout_file: str, data_spec_file: str, tooltip_file: str,
76         time_period: str=None) -> None:
77         """Initialization:
78         - save the input parameters,
79         - read and pre-process the data,
80         - prepare data for the control panel,
81         - read HTML layout file,
82         - read tooltips from the tooltip file.
83
84         :param app: Flask application running the dash application.
85         :param html_layout_file: Path and name of the file specifying the HTML
86             layout of the dash application.
87         :param graph_layout_file: Path and name of the file with layout of
88             plot.ly graphs.
89         :param data_spec_file: Path and name of the file specifying the data to
90             be read from parquets for this application.
91         :param tooltip_file: Path and name of the yaml file specifying the
92             tooltips.
93         :param time_period: It defines the time period for data read from the
94             parquets in days from now back to the past.
95         :type app: Flask
96         :type html_layout_file: str
97         :type graph_layout_file: str
98         :type data_spec_file: str
99         :type tooltip_file: str
100         :type time_period: int
101         """
102
103         # Inputs
104         self._app = app
105         self._html_layout_file = html_layout_file
106         self._graph_layout_file = graph_layout_file
107         self._data_spec_file = data_spec_file
108         self._tooltip_file = tooltip_file
109         self._time_period = time_period
110
111         # Read the data:
112         data_mrr = Data(
113             data_spec_file=self._data_spec_file,
114             debug=True
115         ).read_trending_mrr(days=self._time_period)
116
117         data_ndrpdr = Data(
118             data_spec_file=self._data_spec_file,
119             debug=True
120         ).read_trending_ndrpdr(days=self._time_period)
121
122         self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
123
124         # Get structure of tests:
125         tbs = dict()
126         for _, row in self._data[["job", "test_id"]].drop_duplicates().\
127                 iterrows():
128             lst_job = row["job"].split("-")
129             dut = lst_job[1]
130             ttype = lst_job[3]
131             tbed = "-".join(lst_job[-2:])
132             lst_test = row["test_id"].split(".")
133             if dut == "dpdk":
134                 area = "dpdk"
135             else:
136                 area = "-".join(lst_test[3:-2])
137             suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\
138                 replace("2n-", "")
139             test = lst_test[-1]
140             nic = suite.split("-")[0]
141             for drv in C.DRIVERS:
142                 if drv in test:
143                     if drv == "af-xdp":
144                         driver = "af_xdp"
145                     else:
146                         driver = drv
147                     test = test.replace(f"{drv}-", "")
148                     break
149             else:
150                 driver = "dpdk"
151             infra = "-".join((tbed, nic, driver))
152             lst_test = test.split("-")
153             framesize = lst_test[0]
154             core = lst_test[1] if lst_test[1] else "8C"
155             test = "-".join(lst_test[2: -1])
156
157             if tbs.get(dut, None) is None:
158                 tbs[dut] = dict()
159             if tbs[dut].get(infra, None) is None:
160                 tbs[dut][infra] = dict()
161             if tbs[dut][infra].get(area, None) is None:
162                 tbs[dut][infra][area] = dict()
163             if tbs[dut][infra][area].get(test, None) is None:
164                 tbs[dut][infra][area][test] = dict()
165                 tbs[dut][infra][area][test]["core"] = list()
166                 tbs[dut][infra][area][test]["frame-size"] = list()
167                 tbs[dut][infra][area][test]["test-type"] = list()
168             if core.upper() not in tbs[dut][infra][area][test]["core"]:
169                 tbs[dut][infra][area][test]["core"].append(core.upper())
170             if framesize.upper() not in \
171                     tbs[dut][infra][area][test]["frame-size"]:
172                 tbs[dut][infra][area][test]["frame-size"].append(
173                     framesize.upper()
174                 )
175             if ttype == "mrr":
176                 if "MRR" not in tbs[dut][infra][area][test]["test-type"]:
177                     tbs[dut][infra][area][test]["test-type"].append("MRR")
178             elif ttype == "ndrpdr":
179                 if "NDR" not in tbs[dut][infra][area][test]["test-type"]:
180                     tbs[dut][infra][area][test]["test-type"].extend(
181                         ("NDR", "PDR")
182                     )
183         self._spec_tbs = tbs
184
185         # Read from files:
186         self._html_layout = str()
187         self._graph_layout = None
188         self._tooltips = dict()
189
190         try:
191             with open(self._html_layout_file, "r") as file_read:
192                 self._html_layout = file_read.read()
193         except IOError as err:
194             raise RuntimeError(
195                 f"Not possible to open the file {self._html_layout_file}\n{err}"
196             )
197
198         try:
199             with open(self._graph_layout_file, "r") as file_read:
200                 self._graph_layout = load(file_read, Loader=FullLoader)
201         except IOError as err:
202             raise RuntimeError(
203                 f"Not possible to open the file {self._graph_layout_file}\n"
204                 f"{err}"
205             )
206         except YAMLError as err:
207             raise RuntimeError(
208                 f"An error occurred while parsing the specification file "
209                 f"{self._graph_layout_file}\n{err}"
210             )
211
212         try:
213             with open(self._tooltip_file, "r") as file_read:
214                 self._tooltips = load(file_read, Loader=FullLoader)
215         except IOError as err:
216             logging.warning(
217                 f"Not possible to open the file {self._tooltip_file}\n{err}"
218             )
219         except YAMLError as err:
220             logging.warning(
221                 f"An error occurred while parsing the specification file "
222                 f"{self._tooltip_file}\n{err}"
223             )
224
225         # Callbacks:
226         if self._app is not None and hasattr(self, "callbacks"):
227             self.callbacks(self._app)
228
229     @property
230     def html_layout(self):
231         return self._html_layout
232
233     def add_content(self):
234         """Top level method which generated the web page.
235
236         It generates:
237         - Store for user input data,
238         - Navigation bar,
239         - Main area with control panel and ploting area.
240
241         If no HTML layout is provided, an error message is displayed instead.
242
243         :returns: The HTML div with the whole page.
244         :rtype: html.Div
245         """
246
247         if self.html_layout and self._spec_tbs:
248             return html.Div(
249                 id="div-main",
250                 className="small",
251                 children=[
252                     dbc.Row(
253                         id="row-navbar",
254                         class_name="g-0",
255                         children=[
256                             self._add_navbar()
257                         ]
258                     ),
259                     dcc.Loading(
260                         dbc.Offcanvas(
261                             class_name="w-50",
262                             id="offcanvas-metadata",
263                             title="Throughput And Latency",
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                     ),
272                     dbc.Row(
273                         id="row-main",
274                         class_name="g-0",
275                         children=[
276                             dcc.Store(id="store-selected-tests"),
277                             dcc.Store(id="store-control-panel"),
278                             dcc.Location(id="url", refresh=False),
279                             self._add_ctrl_col(),
280                             self._add_plotting_col()
281                         ]
282                     )
283                 ]
284             )
285         else:
286             return html.Div(
287                 id="div-main-error",
288                 children=[
289                     dbc.Alert(
290                         [
291                             "An Error Occured"
292                         ],
293                         color="danger"
294                     )
295                 ]
296             )
297
298     def _add_navbar(self):
299         """Add nav element with navigation panel. It is placed on the top.
300
301         :returns: Navigation bar.
302         :rtype: dbc.NavbarSimple
303         """
304         return dbc.NavbarSimple(
305             id="navbarsimple-main",
306             children=[
307                 dbc.NavItem(
308                     dbc.NavLink(
309                         C.TREND_TITLE,
310                         disabled=True,
311                         external_link=True,
312                         href="#"
313                     )
314                 )
315             ],
316             brand=C.BRAND,
317             brand_href="/",
318             brand_external_link=True,
319             class_name="p-2",
320             fluid=True
321         )
322
323     def _add_ctrl_col(self) -> dbc.Col:
324         """Add column with controls. It is placed on the left side.
325
326         :returns: Column with the control panel.
327         :rtype: dbc.Col
328         """
329         return dbc.Col([
330             html.Div(
331                 children=self._add_ctrl_panel(),
332                 className="sticky-top"
333             )
334         ])
335
336     def _add_plotting_col(self) -> dbc.Col:
337         """Add column with plots and tables. It is placed on the right side.
338
339         :returns: Column with tables.
340         :rtype: dbc.Col
341         """
342         return dbc.Col(
343             id="col-plotting-area",
344             children=[
345                 dcc.Loading(
346                     children=[
347                         dbc.Row(
348                             id="plotting-area",
349                             class_name="g-0 p-0",
350                             children=[
351                                 C.PLACEHOLDER
352                             ]
353                         )
354                     ]
355                 )
356             ],
357             width=9
358         )
359
360     def _add_ctrl_panel(self) -> list:
361         """Add control panel.
362
363         :returns: Control panel.
364         :rtype: list
365         """
366         return [
367             dbc.Row(
368                 class_name="g-0 p-1",
369                 children=[
370                     dbc.InputGroup(
371                         [
372                             dbc.InputGroupText(
373                                 children=show_tooltip(
374                                     self._tooltips,
375                                     "help-dut",
376                                     "DUT"
377                                 )
378                             ),
379                             dbc.Select(
380                                 id={"type": "ctrl-dd", "index": "dut"},
381                                 placeholder="Select a Device under Test...",
382                                 options=sorted(
383                                     [
384                                         {"label": k, "value": k} \
385                                             for k in self._spec_tbs.keys()
386                                     ],
387                                     key=lambda d: d["label"]
388                                 )
389                             )
390                         ],
391                         size="sm"
392                     )
393                 ]
394             ),
395             dbc.Row(
396                 class_name="g-0 p-1",
397                 children=[
398                     dbc.InputGroup(
399                         [
400                             dbc.InputGroupText(
401                                 children=show_tooltip(
402                                     self._tooltips,
403                                     "help-infra",
404                                     "Infra"
405                                 )
406                             ),
407                             dbc.Select(
408                                 id={"type": "ctrl-dd", "index": "phy"},
409                                 placeholder=\
410                                     "Select a Physical Test Bed Topology..."
411                             )
412                         ],
413                         size="sm"
414                     )
415                 ]
416             ),
417             dbc.Row(
418                 class_name="g-0 p-1",
419                 children=[
420                     dbc.InputGroup(
421                         [
422                             dbc.InputGroupText(
423                                 children=show_tooltip(
424                                     self._tooltips,
425                                     "help-area",
426                                     "Area"
427                                 )
428                             ),
429                             dbc.Select(
430                                 id={"type": "ctrl-dd", "index": "area"},
431                                 placeholder="Select an Area..."
432                             )
433                         ],
434                         size="sm"
435                     )
436                 ]
437             ),
438             dbc.Row(
439                 class_name="g-0 p-1",
440                 children=[
441                     dbc.InputGroup(
442                         [
443                             dbc.InputGroupText(
444                                 children=show_tooltip(
445                                     self._tooltips,
446                                     "help-test",
447                                     "Test"
448                                 )
449                             ),
450                             dbc.Select(
451                                 id={"type": "ctrl-dd", "index": "test"},
452                                 placeholder="Select a Test..."
453                             )
454                         ],
455                         size="sm"
456                     )
457                 ]
458             ),
459             dbc.Row(
460                 class_name="g-0 p-1",
461                 children=[
462                     dbc.Label(
463                         children=show_tooltip(
464                             self._tooltips,
465                             "help-framesize",
466                             "Frame Size"
467                         )
468                     ),
469                     dbc.Col(
470                         children=[
471                             dbc.Checklist(
472                                 id={"type": "ctrl-cl", "index": "frmsize-all"},
473                                 options=C.CL_ALL_DISABLED,
474                                 inline=True,
475                                 switch=False,
476                                 input_class_name="border-info bg-info"
477                             )
478                         ],
479                         width=3
480                     ),
481                     dbc.Col(
482                         children=[
483                             dbc.Checklist(
484                                 id={"type": "ctrl-cl", "index": "frmsize"},
485                                 inline=True,
486                                 switch=False,
487                                 input_class_name="border-info bg-info"
488                             )
489                         ]
490                     )
491                 ]
492             ),
493             dbc.Row(
494                 class_name="g-0 p-1",
495                 children=[
496                     dbc.Label(
497                         children=show_tooltip(
498                             self._tooltips,
499                             "help-cores",
500                             "Number of Cores"
501                         )
502                     ),
503                     dbc.Col(
504                         children=[
505                             dbc.Checklist(
506                                 id={"type": "ctrl-cl", "index": "core-all"},
507                                 options=C.CL_ALL_DISABLED,
508                                 inline=False,
509                                 switch=False,
510                                 input_class_name="border-info bg-info"
511                             )
512                         ],
513                         width=3
514                     ),
515                     dbc.Col(
516                         children=[
517                             dbc.Checklist(
518                                 id={"type": "ctrl-cl", "index": "core"},
519                                 inline=True,
520                                 switch=False,
521                                 input_class_name="border-info bg-info"
522                             )
523                         ]
524                     )
525                 ]
526             ),
527             dbc.Row(
528                 class_name="g-0 p-1",
529                 children=[
530                     dbc.Label(
531                         children=show_tooltip(
532                             self._tooltips,
533                             "help-ttype",
534                             "Test Type"
535                         )
536                     ),
537                     dbc.Col(
538                         children=[
539                             dbc.Checklist(
540                                 id={"type": "ctrl-cl", "index": "tsttype-all"},
541                                 options=C.CL_ALL_DISABLED,
542                                 inline=True,
543                                 switch=False,
544                                 input_class_name="border-info bg-info"
545                             )
546                         ],
547                         width=3
548                     ),
549                     dbc.Col(
550                         children=[
551                             dbc.Checklist(
552                                 id={"type": "ctrl-cl", "index": "tsttype"},
553                                 inline=True,
554                                 switch=False,
555                                 input_class_name="border-info bg-info"
556                             )
557                         ]
558                     )
559                 ]
560             ),
561             dbc.Row(
562                 class_name="g-0 p-1",
563                 children=[
564                     dbc.Label(
565                         children=show_tooltip(
566                             self._tooltips,
567                             "help-normalize",
568                             "Normalize"
569                         )
570                     ),
571                     dbc.Col(
572                         children=[
573                             dbc.Checklist(
574                                 id="normalize",
575                                 options=[
576                                     {
577                                         "value": "normalize",
578                                         "label": (
579                                             "Normalize results to CPU "
580                                             "frequency 2GHz"
581                                         )
582                                     }
583                                 ],
584                                 value=[],
585                                 inline=True,
586                                 switch=False,
587                                 input_class_name="border-info bg-info"
588                             )
589                         ]
590                     )
591                 ]
592             ),
593             dbc.Row(
594                 class_name="g-0 p-1",
595                 children=[
596                     dbc.Button(
597                         id={"type": "ctrl-btn", "index": "add-test"},
598                         children="Add Selected",
599                         color="info"
600                     )
601                 ]
602             ),
603             dbc.Row(
604                 id="row-card-sel-tests",
605                 class_name="g-0 p-1",
606                 style=C.STYLE_DISABLED,
607                 children=[
608                     dbc.ListGroup(
609                         class_name="overflow-auto p-0",
610                         id="lg-selected",
611                         children=[],
612                         style={"max-height": "14em"},
613                         flush=True
614                     )
615                 ]
616             ),
617             dbc.Row(
618                 id="row-btns-sel-tests",
619                 class_name="g-0 p-1",
620                 style=C.STYLE_DISABLED,
621                 children=[
622                     dbc.ButtonGroup(
623                         children=[
624                             dbc.Button(
625                                 id={"type": "ctrl-btn", "index": "rm-test"},
626                                 children="Remove Selected",
627                                 class_name="w-100",
628                                 color="info",
629                                 disabled=False
630                             ),
631                             dbc.Button(
632                                 id={"type": "ctrl-btn", "index": "rm-test-all"},
633                                 children="Remove All",
634                                 class_name="w-100",
635                                 color="info",
636                                 disabled=False
637                             )
638                         ]
639                     )
640                 ]
641             )
642         ]
643
644     def _get_plotting_area(
645             self,
646             tests: list,
647             normalize: bool,
648             url: str
649         ) -> list:
650         """Generate the plotting area with all its content.
651         """
652         if not tests:
653             return C.PLACEHOLDER
654
655         figs = graph_trending(self._data, tests, self._graph_layout, normalize)
656
657         if not figs[0]:
658             return C.PLACEHOLDER
659
660         tab_items = [
661             dbc.Tab(
662                 children=dcc.Graph(
663                     id={"type": "graph", "index": "tput"},
664                     figure=figs[0]
665                 ),
666                 label="Throughput",
667                 tab_id="tab-tput"
668             )
669         ]
670
671         if figs[1]:
672             tab_items.append(
673                 dbc.Tab(
674                     children=dcc.Graph(
675                         id={"type": "graph", "index": "lat"},
676                         figure=figs[1]
677                     ),
678                     label="Latency",
679                     tab_id="tab-lat"
680                 )
681             )
682
683         trending = [
684             dbc.Row(
685                 children=dbc.Tabs(
686                     children=tab_items,
687                     id="tabs",
688                     active_tab="tab-tput",
689                 )
690             ),
691             dbc.Row(
692                 [
693                     dbc.Col([html.Div(
694                         [
695                             dbc.Button(
696                                 id="plot-btn-url",
697                                 children="URL",
698                                 class_name="me-1",
699                                 color="info",
700                                 style={
701                                     "text-transform": "none",
702                                     "padding": "0rem 1rem"
703                                 }
704                             ),
705                             dbc.Modal(
706                                 [
707                                     dbc.ModalHeader(dbc.ModalTitle("URL")),
708                                     dbc.ModalBody(url)
709                                 ],
710                                 id="plot-mod-url",
711                                 size="xl",
712                                 is_open=False,
713                                 scrollable=True
714                             ),
715                             dbc.Button(
716                                 id="plot-btn-download",
717                                 children="Download Data",
718                                 class_name="me-1",
719                                 color="info",
720                                 style={
721                                     "text-transform": "none",
722                                     "padding": "0rem 1rem"
723                                 }
724                             ),
725                             dcc.Download(id="download-trending-data")
726                         ],
727                         className=\
728                             "d-grid gap-0 d-md-flex justify-content-md-end"
729                     )])
730                 ],
731                 class_name="g-0 p-0"
732             )
733         ]
734
735         acc_items = [
736             dbc.AccordionItem(
737                 title="Trending",
738                 children=trending
739             )
740         ]
741
742         return dbc.Col(
743             children=[
744                 dbc.Row(
745                     dbc.Accordion(
746                         children=acc_items,
747                         class_name="g-0 p-1",
748                         start_collapsed=False,
749                         always_open=True,
750                         active_item=[f"item-{i}" for i in range(len(acc_items))]
751                     ),
752                     class_name="g-0 p-0",
753                 ),
754                 # dbc.Row(
755                 #     dbc.Col([html.Div(
756                 #         [
757                 #             dbc.Button(
758                 #                 id="btn-add-telemetry",
759                 #                 children="Add Panel with Telemetry",
760                 #                 class_name="me-1",
761                 #                 color="info",
762                 #                 style={
763                 #                     "text-transform": "none",
764                 #                     "padding": "0rem 1rem"
765                 #                 }
766                 #             )
767                 #         ],
768                 #         className=\
769                 #             "d-grid gap-0 d-md-flex justify-content-md-end"
770                 #     )]),
771                 #     class_name="g-0 p-0"
772                 # )
773             ]
774         )
775
776     def callbacks(self, app):
777         """Callbacks for the whole application.
778
779         :param app: The application.
780         :type app: Flask
781         """
782         
783         @app.callback(
784             [
785                 Output("store-control-panel", "data"),
786                 Output("store-selected-tests", "data"),
787                 Output("plotting-area", "children"),
788                 Output("row-card-sel-tests", "style"),
789                 Output("row-btns-sel-tests", "style"),
790                 Output("lg-selected", "children"),
791
792                 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
793                 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
794                 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
795                 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
796                 Output({"type": "ctrl-dd", "index": "area"}, "options"),
797                 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
798                 Output({"type": "ctrl-dd", "index": "area"}, "value"),
799                 Output({"type": "ctrl-dd", "index": "test"}, "options"),
800                 Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
801                 Output({"type": "ctrl-dd", "index": "test"}, "value"),
802                 Output({"type": "ctrl-cl", "index": "core"}, "options"),
803                 Output({"type": "ctrl-cl", "index": "core"}, "value"),
804                 Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
805                 Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
806                 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
807                 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
808                 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
809                 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
810                 Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
811                 Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
812                 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
813                 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
814                 Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
815                 Output("normalize", "value")
816             ],
817             [
818                 State("store-control-panel", "data"),
819                 State("store-selected-tests", "data"),
820                 State({"type": "sel-cl", "index": ALL}, "value")
821             ],
822             [
823                 Input("url", "href"),
824                 Input("normalize", "value"),
825
826                 Input({"type": "ctrl-dd", "index": ALL}, "value"),
827                 Input({"type": "ctrl-cl", "index": ALL}, "value"),
828                 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
829             ]
830         )
831         def _update_application(
832                 control_panel: dict,
833                 store_sel: list,
834                 lst_sel: list,
835                 href: str,
836                 normalize: list,
837                 *_
838             ) -> tuple:
839             """Update the application when the event is detected.
840             """
841
842             ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
843             on_draw = False
844
845             # Parse the url:
846             parsed_url = url_decode(href)
847             if parsed_url:
848                 url_params = parsed_url["params"]
849             else:
850                 url_params = None
851
852             plotting_area = no_update
853             row_card_sel_tests = no_update
854             row_btns_sel_tests = no_update
855             lg_selected = no_update
856
857             trigger = Trigger(callback_context.triggered)
858
859             if trigger.type == "url" and url_params:
860                 try:
861                     store_sel = literal_eval(url_params["store_sel"][0])
862                     normalize = literal_eval(url_params["norm"][0])
863                 except (KeyError, IndexError):
864                     pass
865                 if store_sel:
866                     last_test = store_sel[-1]
867                     test = self._spec_tbs[last_test["dut"]][last_test["phy"]]\
868                         [last_test["area"]][last_test["test"]]
869                     ctrl_panel.set({
870                         "dd-dut-val": last_test["dut"],
871                         "dd-phy-val": last_test["phy"],
872                         "dd-phy-opt": generate_options(
873                             self._spec_tbs[last_test["dut"]].keys()
874                         ),
875                         "dd-phy-dis": False,
876                         "dd-area-val": last_test["area"],
877                         "dd-area-opt": [
878                             {"label": label(v), "value": v} for v in sorted(
879                                 self._spec_tbs[last_test["dut"]]\
880                                     [last_test["phy"]].keys()
881                             )
882                         ],
883                         "dd-area-dis": False,
884                         "dd-test-val": last_test["test"],
885                         "dd-test-opt": generate_options(
886                             self._spec_tbs[last_test["dut"]][last_test["phy"]]\
887                                 [last_test["area"]].keys()
888                         ),
889                         "dd-test-dis": False,
890                         "cl-core-opt": generate_options(test["core"]),
891                         "cl-core-val": [last_test["core"].upper(), ],
892                         "cl-core-all-val": list(),
893                         "cl-core-all-opt": C.CL_ALL_ENABLED,
894                         "cl-frmsize-opt": generate_options(test["frame-size"]),
895                         "cl-frmsize-val": [last_test["framesize"].upper(), ],
896                         "cl-frmsize-all-val": list(),
897                         "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
898                         "cl-tsttype-opt": generate_options(test["test-type"]),
899                         "cl-tsttype-val": [last_test["testtype"].upper(), ],
900                         "cl-tsttype-all-val": list(),
901                         "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
902                         "cl-normalize-val": normalize,
903                         "btn-add-dis": False
904                     })
905                     on_draw = True
906             elif trigger.type == "normalize":
907                 ctrl_panel.set({"cl-normalize-val": normalize})
908                 on_draw = True
909             elif trigger.type == "ctrl-dd":
910                 if trigger.idx == "dut":
911                     try:
912                         options = generate_options(
913                             self._spec_tbs[trigger.value].keys()
914                         )
915                         disabled = False
916                     except KeyError:
917                         options = list()
918                         disabled = True
919                     ctrl_panel.set({
920                         "dd-dut-val": trigger.value,
921                         "dd-phy-val": str(),
922                         "dd-phy-opt": options,
923                         "dd-phy-dis": disabled,
924                         "dd-area-val": str(),
925                         "dd-area-opt": list(),
926                         "dd-area-dis": True,
927                         "dd-test-val": str(),
928                         "dd-test-opt": list(),
929                         "dd-test-dis": True,
930                         "cl-core-opt": list(),
931                         "cl-core-val": list(),
932                         "cl-core-all-val": list(),
933                         "cl-core-all-opt": C.CL_ALL_DISABLED,
934                         "cl-frmsize-opt": list(),
935                         "cl-frmsize-val": list(),
936                         "cl-frmsize-all-val": list(),
937                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
938                         "cl-tsttype-opt": list(),
939                         "cl-tsttype-val": list(),
940                         "cl-tsttype-all-val": list(),
941                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
942                         "btn-add-dis": True
943                     })
944                 elif trigger.idx == "phy":
945                     try:
946                         dut = ctrl_panel.get("dd-dut-val")
947                         phy = self._spec_tbs[dut][trigger.value]
948                         options = [{"label": label(v), "value": v} \
949                             for v in sorted(phy.keys())]
950                         disabled = False
951                     except KeyError:
952                         options = list()
953                         disabled = True
954                     ctrl_panel.set({
955                         "dd-phy-val": trigger.value,
956                         "dd-area-val": str(),
957                         "dd-area-opt": options,
958                         "dd-area-dis": disabled,
959                         "dd-test-val": str(),
960                         "dd-test-opt": list(),
961                         "dd-test-dis": True,
962                         "cl-core-opt": list(),
963                         "cl-core-val": list(),
964                         "cl-core-all-val": list(),
965                         "cl-core-all-opt": C.CL_ALL_DISABLED,
966                         "cl-frmsize-opt": list(),
967                         "cl-frmsize-val": list(),
968                         "cl-frmsize-all-val": list(),
969                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
970                         "cl-tsttype-opt": list(),
971                         "cl-tsttype-val": list(),
972                         "cl-tsttype-all-val": list(),
973                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
974                         "btn-add-dis": True
975                     })  
976                 elif trigger.idx == "area":
977                     try:
978                         dut = ctrl_panel.get("dd-dut-val")
979                         phy = ctrl_panel.get("dd-phy-val")
980                         area = self._spec_tbs[dut][phy][trigger.value]
981                         options = generate_options(area.keys())
982                         disabled = False
983                     except KeyError:
984                         options = list()
985                         disabled = True
986                     ctrl_panel.set({
987                         "dd-area-val": trigger.value,
988                         "dd-test-val": str(),
989                         "dd-test-opt": options,
990                         "dd-test-dis": disabled,
991                         "cl-core-opt": list(),
992                         "cl-core-val": list(),
993                         "cl-core-all-val": list(),
994                         "cl-core-all-opt": C.CL_ALL_DISABLED,
995                         "cl-frmsize-opt": list(),
996                         "cl-frmsize-val": list(),
997                         "cl-frmsize-all-val": list(),
998                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
999                         "cl-tsttype-opt": list(),
1000                         "cl-tsttype-val": list(),
1001                         "cl-tsttype-all-val": list(),
1002                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1003                         "btn-add-dis": True
1004                     })
1005                 elif trigger.idx == "test":
1006                     dut = ctrl_panel.get("dd-dut-val")
1007                     phy = ctrl_panel.get("dd-phy-val")
1008                     area = ctrl_panel.get("dd-area-val")
1009                     if all((dut, phy, area, trigger.value, )):
1010                         test = self._spec_tbs[dut][phy][area][trigger.value]
1011                         ctrl_panel.set({
1012                             "dd-test-val": trigger.value,
1013                             "cl-core-opt": generate_options(test["core"]),
1014                             "cl-core-val": list(),
1015                             "cl-core-all-val": list(),
1016                             "cl-core-all-opt": C.CL_ALL_ENABLED,
1017                             "cl-frmsize-opt": \
1018                                 generate_options(test["frame-size"]),
1019                             "cl-frmsize-val": list(),
1020                             "cl-frmsize-all-val": list(),
1021                             "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1022                             "cl-tsttype-opt": \
1023                                 generate_options(test["test-type"]),
1024                             "cl-tsttype-val": list(),
1025                             "cl-tsttype-all-val": list(),
1026                             "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1027                             "btn-add-dis": True
1028                         })
1029             elif trigger.type == "ctrl-cl":
1030                 param = trigger.idx.split("-")[0]
1031                 if "-all" in trigger.idx:
1032                     c_sel, c_all, c_id = list(), trigger.value, "all"
1033                 else:
1034                     c_sel, c_all, c_id = trigger.value, list(), str()
1035                 val_sel, val_all = sync_checklists(
1036                     options=ctrl_panel.get(f"cl-{param}-opt"),
1037                     sel=c_sel,
1038                     all=c_all,
1039                     id=c_id
1040                 )
1041                 ctrl_panel.set({
1042                     f"cl-{param}-val": val_sel,
1043                     f"cl-{param}-all-val": val_all,
1044                 })
1045                 if all((ctrl_panel.get("cl-core-val"), 
1046                         ctrl_panel.get("cl-frmsize-val"),
1047                         ctrl_panel.get("cl-tsttype-val"), )):
1048                     ctrl_panel.set({"btn-add-dis": False})
1049                 else:
1050                     ctrl_panel.set({"btn-add-dis": True})
1051             elif trigger.type == "ctrl-btn":
1052                 on_draw = True
1053                 if trigger.idx == "add-test":
1054                     dut = ctrl_panel.get("dd-dut-val")
1055                     phy = ctrl_panel.get("dd-phy-val")
1056                     area = ctrl_panel.get("dd-area-val")
1057                     test = ctrl_panel.get("dd-test-val")
1058                     # Add selected test(s) to the list of tests in store:
1059                     if store_sel is None:
1060                         store_sel = list()
1061                     for core in ctrl_panel.get("cl-core-val"):
1062                         for framesize in ctrl_panel.get("cl-frmsize-val"):
1063                             for ttype in ctrl_panel.get("cl-tsttype-val"):
1064                                 if dut == "trex":
1065                                     core = str()
1066                                 tid = "-".join((
1067                                     dut,
1068                                     phy.replace('af_xdp', 'af-xdp'),
1069                                     area,
1070                                     framesize.lower(),
1071                                     core.lower(),
1072                                     test,
1073                                     ttype.lower()
1074                                 ))
1075                                 if tid not in [i["id"] for i in store_sel]:
1076                                     store_sel.append({
1077                                         "id": tid,
1078                                         "dut": dut,
1079                                         "phy": phy,
1080                                         "area": area,
1081                                         "test": test,
1082                                         "framesize": framesize.lower(),
1083                                         "core": core.lower(),
1084                                         "testtype": ttype.lower()
1085                                     })
1086                     store_sel = sorted(store_sel, key=lambda d: d["id"])
1087                     if C.CLEAR_ALL_INPUTS:
1088                         ctrl_panel.set(ctrl_panel.defaults)
1089                 elif trigger.idx == "rm-test" and lst_sel:
1090                     new_store_sel = list()
1091                     for idx, item in enumerate(store_sel):
1092                         if not lst_sel[idx]:
1093                             new_store_sel.append(item)
1094                     store_sel = new_store_sel
1095                 elif trigger.idx == "rm-test-all":
1096                     store_sel = list()
1097                 
1098             if on_draw:
1099                 if store_sel:
1100                     lg_selected = get_list_group_items(store_sel)
1101                     plotting_area = self._get_plotting_area(
1102                         store_sel,
1103                         bool(normalize),
1104                         gen_new_url(
1105                             parsed_url,
1106                             {"store_sel": store_sel, "norm": normalize}
1107                         )
1108                     )
1109                     row_card_sel_tests = C.STYLE_ENABLED
1110                     row_btns_sel_tests = C.STYLE_ENABLED
1111                 else:
1112                     plotting_area = C.PLACEHOLDER
1113                     row_card_sel_tests = C.STYLE_DISABLED
1114                     row_btns_sel_tests = C.STYLE_DISABLED
1115                     store_sel = list()
1116
1117             ret_val = [
1118                 ctrl_panel.panel,
1119                 store_sel,
1120                 plotting_area,
1121                 row_card_sel_tests,
1122                 row_btns_sel_tests,
1123                 lg_selected
1124             ]
1125             ret_val.extend(ctrl_panel.values)
1126             return ret_val
1127
1128         @app.callback(
1129             Output("plot-mod-url", "is_open"),
1130             [Input("plot-btn-url", "n_clicks")],
1131             [State("plot-mod-url", "is_open")],
1132         )
1133         def toggle_plot_mod_url(n, is_open):
1134             """Toggle the modal window with url.
1135             """
1136             if n:
1137                 return not is_open
1138             return is_open
1139
1140         @app.callback(
1141             Output("metadata-tput-lat", "children"),
1142             Output("metadata-hdrh-graph", "children"),
1143             Output("offcanvas-metadata", "is_open"),
1144             Input({"type": "graph", "index": ALL}, "clickData"),
1145             prevent_initial_call=True
1146         )
1147         def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1148             """Generates the data for the offcanvas displayed when a particular
1149             point in a graph is clicked on.
1150
1151             :param graph_data: The data from the clicked point in the graph.
1152             :type graph_data: dict
1153             :returns: The data to be displayed on the offcanvas and the
1154                 information to show the offcanvas.
1155             :rtype: tuple(list, list, bool)
1156             """
1157
1158             trigger = Trigger(callback_context.triggered)
1159
1160             try:
1161                 idx = 0 if trigger.idx == "tput" else 1
1162                 graph_data = graph_data[idx]["points"][0]
1163             except (IndexError, KeyError, ValueError, TypeError):
1164                 raise PreventUpdate
1165
1166             metadata = no_update
1167             graph = list()
1168
1169             children = [
1170                 dbc.ListGroupItem(
1171                     [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1172                 ) for x in graph_data.get("text", "").split("<br>")
1173             ]
1174             if trigger.idx == "tput":
1175                 title = "Throughput"
1176             elif trigger.idx == "lat":
1177                 title = "Latency"
1178                 hdrh_data = graph_data.get("customdata", None)
1179                 if hdrh_data:
1180                     graph = [dbc.Card(
1181                         class_name="gy-2 p-0",
1182                         children=[
1183                             dbc.CardHeader(hdrh_data.pop("name")),
1184                             dbc.CardBody(children=[
1185                                 dcc.Graph(
1186                                     id="hdrh-latency-graph",
1187                                     figure=graph_hdrh_latency(
1188                                         hdrh_data, self._graph_layout
1189                                     )
1190                                 )
1191                             ])
1192                         ])
1193                     ]
1194             else:
1195                 raise PreventUpdate
1196
1197             metadata = [
1198                 dbc.Card(
1199                     class_name="gy-2 p-0",
1200                     children=[
1201                         dbc.CardHeader(children=[
1202                             dcc.Clipboard(
1203                                 target_id="tput-lat-metadata",
1204                                 title="Copy",
1205                                 style={"display": "inline-block"}
1206                             ),
1207                             title
1208                         ]),
1209                         dbc.CardBody(
1210                             id="tput-lat-metadata",
1211                             class_name="p-0",
1212                             children=[dbc.ListGroup(children, flush=True), ]
1213                         )
1214                     ]
1215                 )
1216             ]
1217
1218             return metadata, graph, True
1219
1220         @app.callback(
1221             Output("download-trending-data", "data"),
1222             State("store-selected-tests", "data"),
1223             Input("plot-btn-download", "n_clicks"),
1224             prevent_initial_call=True
1225         )
1226         def _download_trending_data(store_sel, _):
1227             """Download the data
1228
1229             :param store_sel: List of tests selected by user stored in the
1230                 browser.
1231             :type store_sel: list
1232             :returns: dict of data frame content (base64 encoded) and meta data
1233                 used by the Download component.
1234             :rtype: dict
1235             """
1236
1237             if not store_sel:
1238                 raise PreventUpdate
1239
1240             df = pd.DataFrame()
1241             for itm in store_sel:
1242                 sel_data = select_trending_data(self._data, itm)
1243                 if sel_data is None:
1244                     continue
1245                 df = pd.concat([df, sel_data], ignore_index=True)
1246
1247             return dcc.send_data_frame(df.to_csv, C.TREND_DOWNLOAD_FILE_NAME)