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