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