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