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