C-Dash: Small changes in styling
[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.InputGroup(
463                         [
464                             dbc.InputGroupText(
465                                 children=show_tooltip(
466                                     self._tooltips,
467                                     "help-framesize",
468                                     "Frame Size"
469                                 )
470                             ),
471                             dbc.Col(
472                                 children=[
473                                     dbc.Checklist(
474                                         id={
475                                             "type": "ctrl-cl",
476                                             "index": "frmsize-all"
477                                         },
478                                         options=C.CL_ALL_DISABLED,
479                                         inline=True,
480                                         class_name="ms-2"
481                                     )
482                                 ],
483                                 width=2
484                             ),
485                             dbc.Col(
486                                 children=[
487                                     dbc.Checklist(
488                                         id={
489                                             "type": "ctrl-cl",
490                                             "index": "frmsize"
491                                         },
492                                         inline=True
493                                     )
494                                 ]
495                             )
496                         ],
497                         style={"align-items": "center"},
498                         size="sm"
499                     )
500                 ]
501             ),
502             dbc.Row(
503                 class_name="g-0 p-1",
504                 children=[
505                     dbc.InputGroup(
506                         [
507                             dbc.InputGroupText(
508                                 children=show_tooltip(
509                                     self._tooltips,
510                                     "help-cores",
511                                     "Number of Cores"
512                                 )
513                             ),
514                             dbc.Col(
515                                 children=[
516                                     dbc.Checklist(
517                                         id={
518                                             "type": "ctrl-cl",
519                                             "index": "core-all"
520                                         },
521                                         options=C.CL_ALL_DISABLED,
522                                         inline=True,
523                                         class_name="ms-2"
524                                     )
525                                 ],
526                                 width=2
527                             ),
528                             dbc.Col(
529                                 children=[
530                                     dbc.Checklist(
531                                         id={
532                                             "type": "ctrl-cl",
533                                             "index": "core"
534                                         },
535                                         inline=True
536                                     )
537                                 ]
538                             )
539                         ],
540                         style={"align-items": "center"},
541                         size="sm"
542                     )
543                 ]
544             ),
545             dbc.Row(
546                 class_name="g-0 p-1",
547                 children=[
548                     dbc.InputGroup(
549                         [
550                             dbc.InputGroupText(
551                                 children=show_tooltip(
552                                     self._tooltips,
553                                     "help-ttype",
554                                     "Test Type"
555                                 )
556                             ),
557                             dbc.Col(
558                                 children=[
559                                     dbc.Checklist(
560                                         id={
561                                             "type": "ctrl-cl",
562                                             "index": "tsttype-all"
563                                         },
564                                         options=C.CL_ALL_DISABLED,
565                                         inline=True,
566                                         class_name="ms-2"
567                                     )
568                                 ],
569                                 width=2
570                             ),
571                             dbc.Col(
572                                 children=[
573                                     dbc.Checklist(
574                                         id={
575                                             "type": "ctrl-cl",
576                                             "index": "tsttype"
577                                         },
578                                         inline=True
579                                     )
580                                 ]
581                             )
582                         ],
583                         style={"align-items": "center"},
584                         size="sm"
585                     )
586                 ]
587             ),
588             dbc.Row(
589                 class_name="g-0 p-1",
590                 children=[
591                     dbc.InputGroup(
592                         [
593                             dbc.InputGroupText(
594                                 children=show_tooltip(
595                                     self._tooltips,
596                                     "help-normalize",
597                                     "Normalization"
598                                 )
599                             ),
600                             dbc.Col(
601                                 children=[
602                                     dbc.Checklist(
603                                         id="normalize",
604                                         options=[{
605                                             "value": "normalize",
606                                             "label": (
607                                                 "Normalize to CPU frequency "
608                                                 "2GHz"
609                                             )
610                                         }],
611                                         value=[],
612                                         inline=True,
613                                         class_name="ms-2"
614                                     )
615                                 ]
616                             )
617                         ],
618                         style={"align-items": "center"},
619                         size="sm"
620                     )
621                 ]
622             ),
623             dbc.Row(
624                 class_name="g-0 p-1",
625                 children=[
626                     dbc.Button(
627                         id={"type": "ctrl-btn", "index": "add-test"},
628                         children="Add Selected",
629                         color="info"
630                     )
631                 ]
632             ),
633             dbc.Row(
634                 id="row-card-sel-tests",
635                 class_name="g-0 p-1",
636                 style=C.STYLE_DISABLED,
637                 children=[
638                     dbc.ListGroup(
639                         class_name="overflow-auto p-0",
640                         id="lg-selected",
641                         children=[],
642                         style={"max-height": "14em"},
643                         flush=True
644                     )
645                 ]
646             ),
647             dbc.Row(
648                 id="row-btns-sel-tests",
649                 class_name="g-0 p-1",
650                 style=C.STYLE_DISABLED,
651                 children=[
652                     dbc.ButtonGroup(
653                         children=[
654                             dbc.Button(
655                                 id={"type": "ctrl-btn", "index": "rm-test"},
656                                 children="Remove Selected",
657                                 class_name="w-100",
658                                 color="info",
659                                 disabled=False
660                             ),
661                             dbc.Button(
662                                 id={"type": "ctrl-btn", "index": "rm-test-all"},
663                                 children="Remove All",
664                                 class_name="w-100",
665                                 color="info",
666                                 disabled=False
667                             )
668                         ]
669                     )
670                 ]
671             )
672         ]
673
674     def _get_plotting_area(
675             self,
676             tests: list,
677             normalize: bool,
678             url: str
679         ) -> list:
680         """Generate the plotting area with all its content.
681         """
682         if not tests:
683             return C.PLACEHOLDER
684
685         figs = graph_trending(self._data, tests, self._graph_layout, normalize)
686
687         if not figs[0]:
688             return C.PLACEHOLDER
689
690         tab_items = [
691             dbc.Tab(
692                 children=dcc.Graph(
693                     id={"type": "graph", "index": "tput"},
694                     figure=figs[0]
695                 ),
696                 label="Throughput",
697                 tab_id="tab-tput"
698             )
699         ]
700
701         if figs[1]:
702             tab_items.append(
703                 dbc.Tab(
704                     children=dcc.Graph(
705                         id={"type": "graph", "index": "lat"},
706                         figure=figs[1]
707                     ),
708                     label="Latency",
709                     tab_id="tab-lat"
710                 )
711             )
712
713         trending = [
714             dbc.Row(
715                 children=dbc.Tabs(
716                     children=tab_items,
717                     id="tabs",
718                     active_tab="tab-tput",
719                 )
720             ),
721             dbc.Row(
722                 [
723                     dbc.Col([html.Div(
724                         [
725                             dbc.Button(
726                                 id="plot-btn-url",
727                                 children="Show URL",
728                                 class_name="me-1",
729                                 color="info",
730                                 style={
731                                     "text-transform": "none",
732                                     "padding": "0rem 1rem"
733                                 }
734                             ),
735                             dbc.Modal(
736                                 [
737                                     dbc.ModalHeader(dbc.ModalTitle("URL")),
738                                     dbc.ModalBody(url)
739                                 ],
740                                 id="plot-mod-url",
741                                 size="xl",
742                                 is_open=False,
743                                 scrollable=True
744                             ),
745                             dbc.Button(
746                                 id="plot-btn-download",
747                                 children="Download Data",
748                                 class_name="me-1",
749                                 color="info",
750                                 style={
751                                     "text-transform": "none",
752                                     "padding": "0rem 1rem"
753                                 }
754                             ),
755                             dcc.Download(id="download-trending-data")
756                         ],
757                         className=\
758                             "d-grid gap-0 d-md-flex justify-content-md-end"
759                     )])
760                 ],
761                 class_name="g-0 p-0"
762             )
763         ]
764
765         acc_items = [
766             dbc.AccordionItem(
767                 title="Trending",
768                 children=trending
769             )
770         ]
771
772         return dbc.Col(
773             children=[
774                 dbc.Row(
775                     dbc.Accordion(
776                         children=acc_items,
777                         class_name="g-0 p-1",
778                         start_collapsed=False,
779                         always_open=True,
780                         active_item=[f"item-{i}" for i in range(len(acc_items))]
781                     ),
782                     class_name="g-0 p-0",
783                 ),
784                 # dbc.Row(
785                 #     dbc.Col([html.Div(
786                 #         [
787                 #             dbc.Button(
788                 #                 id="btn-add-telemetry",
789                 #                 children="Add Panel with Telemetry",
790                 #                 class_name="me-1",
791                 #                 color="info",
792                 #                 style={
793                 #                     "text-transform": "none",
794                 #                     "padding": "0rem 1rem"
795                 #                 }
796                 #             )
797                 #         ],
798                 #         className=\
799                 #             "d-grid gap-0 d-md-flex justify-content-md-end"
800                 #     )]),
801                 #     class_name="g-0 p-0"
802                 # )
803             ]
804         )
805
806     def callbacks(self, app):
807         """Callbacks for the whole application.
808
809         :param app: The application.
810         :type app: Flask
811         """
812         
813         @app.callback(
814             [
815                 Output("store-control-panel", "data"),
816                 Output("store-selected-tests", "data"),
817                 Output("plotting-area", "children"),
818                 Output("row-card-sel-tests", "style"),
819                 Output("row-btns-sel-tests", "style"),
820                 Output("lg-selected", "children"),
821
822                 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
823                 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
824                 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
825                 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
826                 Output({"type": "ctrl-dd", "index": "area"}, "options"),
827                 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
828                 Output({"type": "ctrl-dd", "index": "area"}, "value"),
829                 Output({"type": "ctrl-dd", "index": "test"}, "options"),
830                 Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
831                 Output({"type": "ctrl-dd", "index": "test"}, "value"),
832                 Output({"type": "ctrl-cl", "index": "core"}, "options"),
833                 Output({"type": "ctrl-cl", "index": "core"}, "value"),
834                 Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
835                 Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
836                 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
837                 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
838                 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
839                 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
840                 Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
841                 Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
842                 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
843                 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
844                 Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
845                 Output("normalize", "value")
846             ],
847             [
848                 State("store-control-panel", "data"),
849                 State("store-selected-tests", "data"),
850                 State({"type": "sel-cl", "index": ALL}, "value")
851             ],
852             [
853                 Input("url", "href"),
854                 Input("normalize", "value"),
855
856                 Input({"type": "ctrl-dd", "index": ALL}, "value"),
857                 Input({"type": "ctrl-cl", "index": ALL}, "value"),
858                 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
859             ]
860         )
861         def _update_application(
862                 control_panel: dict,
863                 store_sel: list,
864                 lst_sel: list,
865                 href: str,
866                 normalize: list,
867                 *_
868             ) -> tuple:
869             """Update the application when the event is detected.
870             """
871
872             ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
873             on_draw = False
874
875             # Parse the url:
876             parsed_url = url_decode(href)
877             if parsed_url:
878                 url_params = parsed_url["params"]
879             else:
880                 url_params = None
881
882             plotting_area = no_update
883             row_card_sel_tests = no_update
884             row_btns_sel_tests = no_update
885             lg_selected = no_update
886
887             trigger = Trigger(callback_context.triggered)
888
889             if trigger.type == "url" and url_params:
890                 try:
891                     store_sel = literal_eval(url_params["store_sel"][0])
892                     normalize = literal_eval(url_params["norm"][0])
893                 except (KeyError, IndexError):
894                     pass
895                 if store_sel:
896                     last_test = store_sel[-1]
897                     test = self._spec_tbs[last_test["dut"]][last_test["phy"]]\
898                         [last_test["area"]][last_test["test"]]
899                     ctrl_panel.set({
900                         "dd-dut-val": last_test["dut"],
901                         "dd-phy-val": last_test["phy"],
902                         "dd-phy-opt": generate_options(
903                             self._spec_tbs[last_test["dut"]].keys()
904                         ),
905                         "dd-phy-dis": False,
906                         "dd-area-val": last_test["area"],
907                         "dd-area-opt": [
908                             {"label": label(v), "value": v} for v in sorted(
909                                 self._spec_tbs[last_test["dut"]]\
910                                     [last_test["phy"]].keys()
911                             )
912                         ],
913                         "dd-area-dis": False,
914                         "dd-test-val": last_test["test"],
915                         "dd-test-opt": generate_options(
916                             self._spec_tbs[last_test["dut"]][last_test["phy"]]\
917                                 [last_test["area"]].keys()
918                         ),
919                         "dd-test-dis": False,
920                         "cl-core-opt": generate_options(test["core"]),
921                         "cl-core-val": [last_test["core"].upper(), ],
922                         "cl-core-all-val": list(),
923                         "cl-core-all-opt": C.CL_ALL_ENABLED,
924                         "cl-frmsize-opt": generate_options(test["frame-size"]),
925                         "cl-frmsize-val": [last_test["framesize"].upper(), ],
926                         "cl-frmsize-all-val": list(),
927                         "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
928                         "cl-tsttype-opt": generate_options(test["test-type"]),
929                         "cl-tsttype-val": [last_test["testtype"].upper(), ],
930                         "cl-tsttype-all-val": list(),
931                         "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
932                         "cl-normalize-val": normalize,
933                         "btn-add-dis": False
934                     })
935                     on_draw = True
936             elif trigger.type == "normalize":
937                 ctrl_panel.set({"cl-normalize-val": normalize})
938                 on_draw = True
939             elif trigger.type == "ctrl-dd":
940                 if trigger.idx == "dut":
941                     try:
942                         options = generate_options(
943                             self._spec_tbs[trigger.value].keys()
944                         )
945                         disabled = False
946                     except KeyError:
947                         options = list()
948                         disabled = True
949                     ctrl_panel.set({
950                         "dd-dut-val": trigger.value,
951                         "dd-phy-val": str(),
952                         "dd-phy-opt": options,
953                         "dd-phy-dis": disabled,
954                         "dd-area-val": str(),
955                         "dd-area-opt": list(),
956                         "dd-area-dis": True,
957                         "dd-test-val": str(),
958                         "dd-test-opt": list(),
959                         "dd-test-dis": True,
960                         "cl-core-opt": list(),
961                         "cl-core-val": list(),
962                         "cl-core-all-val": list(),
963                         "cl-core-all-opt": C.CL_ALL_DISABLED,
964                         "cl-frmsize-opt": list(),
965                         "cl-frmsize-val": list(),
966                         "cl-frmsize-all-val": list(),
967                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
968                         "cl-tsttype-opt": list(),
969                         "cl-tsttype-val": list(),
970                         "cl-tsttype-all-val": list(),
971                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
972                         "btn-add-dis": True
973                     })
974                 elif trigger.idx == "phy":
975                     try:
976                         dut = ctrl_panel.get("dd-dut-val")
977                         phy = self._spec_tbs[dut][trigger.value]
978                         options = [{"label": label(v), "value": v} \
979                             for v in sorted(phy.keys())]
980                         disabled = False
981                     except KeyError:
982                         options = list()
983                         disabled = True
984                     ctrl_panel.set({
985                         "dd-phy-val": trigger.value,
986                         "dd-area-val": str(),
987                         "dd-area-opt": options,
988                         "dd-area-dis": disabled,
989                         "dd-test-val": str(),
990                         "dd-test-opt": list(),
991                         "dd-test-dis": True,
992                         "cl-core-opt": list(),
993                         "cl-core-val": list(),
994                         "cl-core-all-val": list(),
995                         "cl-core-all-opt": C.CL_ALL_DISABLED,
996                         "cl-frmsize-opt": list(),
997                         "cl-frmsize-val": list(),
998                         "cl-frmsize-all-val": list(),
999                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1000                         "cl-tsttype-opt": list(),
1001                         "cl-tsttype-val": list(),
1002                         "cl-tsttype-all-val": list(),
1003                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1004                         "btn-add-dis": True
1005                     })  
1006                 elif trigger.idx == "area":
1007                     try:
1008                         dut = ctrl_panel.get("dd-dut-val")
1009                         phy = ctrl_panel.get("dd-phy-val")
1010                         area = self._spec_tbs[dut][phy][trigger.value]
1011                         options = generate_options(area.keys())
1012                         disabled = False
1013                     except KeyError:
1014                         options = list()
1015                         disabled = True
1016                     ctrl_panel.set({
1017                         "dd-area-val": trigger.value,
1018                         "dd-test-val": str(),
1019                         "dd-test-opt": options,
1020                         "dd-test-dis": disabled,
1021                         "cl-core-opt": list(),
1022                         "cl-core-val": list(),
1023                         "cl-core-all-val": list(),
1024                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1025                         "cl-frmsize-opt": list(),
1026                         "cl-frmsize-val": list(),
1027                         "cl-frmsize-all-val": list(),
1028                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1029                         "cl-tsttype-opt": list(),
1030                         "cl-tsttype-val": list(),
1031                         "cl-tsttype-all-val": list(),
1032                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1033                         "btn-add-dis": True
1034                     })
1035                 elif trigger.idx == "test":
1036                     dut = ctrl_panel.get("dd-dut-val")
1037                     phy = ctrl_panel.get("dd-phy-val")
1038                     area = ctrl_panel.get("dd-area-val")
1039                     if all((dut, phy, area, trigger.value, )):
1040                         test = self._spec_tbs[dut][phy][area][trigger.value]
1041                         ctrl_panel.set({
1042                             "dd-test-val": trigger.value,
1043                             "cl-core-opt": generate_options(test["core"]),
1044                             "cl-core-val": list(),
1045                             "cl-core-all-val": list(),
1046                             "cl-core-all-opt": C.CL_ALL_ENABLED,
1047                             "cl-frmsize-opt": \
1048                                 generate_options(test["frame-size"]),
1049                             "cl-frmsize-val": list(),
1050                             "cl-frmsize-all-val": list(),
1051                             "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1052                             "cl-tsttype-opt": \
1053                                 generate_options(test["test-type"]),
1054                             "cl-tsttype-val": list(),
1055                             "cl-tsttype-all-val": list(),
1056                             "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1057                             "btn-add-dis": True
1058                         })
1059             elif trigger.type == "ctrl-cl":
1060                 param = trigger.idx.split("-")[0]
1061                 if "-all" in trigger.idx:
1062                     c_sel, c_all, c_id = list(), trigger.value, "all"
1063                 else:
1064                     c_sel, c_all, c_id = trigger.value, list(), str()
1065                 val_sel, val_all = sync_checklists(
1066                     options=ctrl_panel.get(f"cl-{param}-opt"),
1067                     sel=c_sel,
1068                     all=c_all,
1069                     id=c_id
1070                 )
1071                 ctrl_panel.set({
1072                     f"cl-{param}-val": val_sel,
1073                     f"cl-{param}-all-val": val_all,
1074                 })
1075                 if all((ctrl_panel.get("cl-core-val"), 
1076                         ctrl_panel.get("cl-frmsize-val"),
1077                         ctrl_panel.get("cl-tsttype-val"), )):
1078                     ctrl_panel.set({"btn-add-dis": False})
1079                 else:
1080                     ctrl_panel.set({"btn-add-dis": True})
1081             elif trigger.type == "ctrl-btn":
1082                 on_draw = True
1083                 if trigger.idx == "add-test":
1084                     dut = ctrl_panel.get("dd-dut-val")
1085                     phy = ctrl_panel.get("dd-phy-val")
1086                     area = ctrl_panel.get("dd-area-val")
1087                     test = ctrl_panel.get("dd-test-val")
1088                     # Add selected test(s) to the list of tests in store:
1089                     if store_sel is None:
1090                         store_sel = list()
1091                     for core in ctrl_panel.get("cl-core-val"):
1092                         for framesize in ctrl_panel.get("cl-frmsize-val"):
1093                             for ttype in ctrl_panel.get("cl-tsttype-val"):
1094                                 if dut == "trex":
1095                                     core = str()
1096                                 tid = "-".join((
1097                                     dut,
1098                                     phy.replace('af_xdp', 'af-xdp'),
1099                                     area,
1100                                     framesize.lower(),
1101                                     core.lower(),
1102                                     test,
1103                                     ttype.lower()
1104                                 ))
1105                                 if tid not in [i["id"] for i in store_sel]:
1106                                     store_sel.append({
1107                                         "id": tid,
1108                                         "dut": dut,
1109                                         "phy": phy,
1110                                         "area": area,
1111                                         "test": test,
1112                                         "framesize": framesize.lower(),
1113                                         "core": core.lower(),
1114                                         "testtype": ttype.lower()
1115                                     })
1116                     store_sel = sorted(store_sel, key=lambda d: d["id"])
1117                     if C.CLEAR_ALL_INPUTS:
1118                         ctrl_panel.set(ctrl_panel.defaults)
1119                 elif trigger.idx == "rm-test" and lst_sel:
1120                     new_store_sel = list()
1121                     for idx, item in enumerate(store_sel):
1122                         if not lst_sel[idx]:
1123                             new_store_sel.append(item)
1124                     store_sel = new_store_sel
1125                 elif trigger.idx == "rm-test-all":
1126                     store_sel = list()
1127                 
1128             if on_draw:
1129                 if store_sel:
1130                     lg_selected = get_list_group_items(store_sel)
1131                     plotting_area = self._get_plotting_area(
1132                         store_sel,
1133                         bool(normalize),
1134                         gen_new_url(
1135                             parsed_url,
1136                             {"store_sel": store_sel, "norm": normalize}
1137                         )
1138                     )
1139                     row_card_sel_tests = C.STYLE_ENABLED
1140                     row_btns_sel_tests = C.STYLE_ENABLED
1141                 else:
1142                     plotting_area = C.PLACEHOLDER
1143                     row_card_sel_tests = C.STYLE_DISABLED
1144                     row_btns_sel_tests = C.STYLE_DISABLED
1145                     store_sel = list()
1146
1147             ret_val = [
1148                 ctrl_panel.panel,
1149                 store_sel,
1150                 plotting_area,
1151                 row_card_sel_tests,
1152                 row_btns_sel_tests,
1153                 lg_selected
1154             ]
1155             ret_val.extend(ctrl_panel.values)
1156             return ret_val
1157
1158         @app.callback(
1159             Output("plot-mod-url", "is_open"),
1160             [Input("plot-btn-url", "n_clicks")],
1161             [State("plot-mod-url", "is_open")],
1162         )
1163         def toggle_plot_mod_url(n, is_open):
1164             """Toggle the modal window with url.
1165             """
1166             if n:
1167                 return not is_open
1168             return is_open
1169
1170         @app.callback(
1171             Output("metadata-tput-lat", "children"),
1172             Output("metadata-hdrh-graph", "children"),
1173             Output("offcanvas-metadata", "is_open"),
1174             Input({"type": "graph", "index": ALL}, "clickData"),
1175             prevent_initial_call=True
1176         )
1177         def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1178             """Generates the data for the offcanvas displayed when a particular
1179             point in a graph is clicked on.
1180
1181             :param graph_data: The data from the clicked point in the graph.
1182             :type graph_data: dict
1183             :returns: The data to be displayed on the offcanvas and the
1184                 information to show the offcanvas.
1185             :rtype: tuple(list, list, bool)
1186             """
1187
1188             trigger = Trigger(callback_context.triggered)
1189
1190             try:
1191                 idx = 0 if trigger.idx == "tput" else 1
1192                 graph_data = graph_data[idx]["points"][0]
1193             except (IndexError, KeyError, ValueError, TypeError):
1194                 raise PreventUpdate
1195
1196             metadata = no_update
1197             graph = list()
1198
1199             children = [
1200                 dbc.ListGroupItem(
1201                     [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
1202                 ) for x in graph_data.get("text", "").split("<br>")
1203             ]
1204             if trigger.idx == "tput":
1205                 title = "Throughput"
1206             elif trigger.idx == "lat":
1207                 title = "Latency"
1208                 hdrh_data = graph_data.get("customdata", None)
1209                 if hdrh_data:
1210                     graph = [dbc.Card(
1211                         class_name="gy-2 p-0",
1212                         children=[
1213                             dbc.CardHeader(hdrh_data.pop("name")),
1214                             dbc.CardBody(children=[
1215                                 dcc.Graph(
1216                                     id="hdrh-latency-graph",
1217                                     figure=graph_hdrh_latency(
1218                                         hdrh_data, self._graph_layout
1219                                     )
1220                                 )
1221                             ])
1222                         ])
1223                     ]
1224             else:
1225                 raise PreventUpdate
1226
1227             metadata = [
1228                 dbc.Card(
1229                     class_name="gy-2 p-0",
1230                     children=[
1231                         dbc.CardHeader(children=[
1232                             dcc.Clipboard(
1233                                 target_id="tput-lat-metadata",
1234                                 title="Copy",
1235                                 style={"display": "inline-block"}
1236                             ),
1237                             title
1238                         ]),
1239                         dbc.CardBody(
1240                             id="tput-lat-metadata",
1241                             class_name="p-0",
1242                             children=[dbc.ListGroup(children, flush=True), ]
1243                         )
1244                     ]
1245                 )
1246             ]
1247
1248             return metadata, graph, True
1249
1250         @app.callback(
1251             Output("download-trending-data", "data"),
1252             State("store-selected-tests", "data"),
1253             Input("plot-btn-download", "n_clicks"),
1254             prevent_initial_call=True
1255         )
1256         def _download_trending_data(store_sel, _):
1257             """Download the data
1258
1259             :param store_sel: List of tests selected by user stored in the
1260                 browser.
1261             :type store_sel: list
1262             :returns: dict of data frame content (base64 encoded) and meta data
1263                 used by the Download component.
1264             :rtype: dict
1265             """
1266
1267             if not store_sel:
1268                 raise PreventUpdate
1269
1270             df = pd.DataFrame()
1271             for itm in store_sel:
1272                 sel_data = select_trending_data(self._data, itm)
1273                 if sel_data is None:
1274                     continue
1275                 df = pd.concat([df, sel_data], ignore_index=True)
1276
1277             return dcc.send_data_frame(df.to_csv, C.TREND_DOWNLOAD_FILE_NAME)