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