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