d85dbe76573edba4feafe0aff836b9e58ae181b5
[csit.git] / resources / tools / dash / app / pal / report / layout.py
1 # Copyright (c) 2022 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Plotly Dash HTML layout override.
15 """
16
17 import logging
18 import pandas as pd
19 import dash_bootstrap_components as dbc
20
21 from flask import Flask
22 from dash import dcc
23 from dash import html
24 from dash import callback_context, no_update, ALL
25 from dash import Input, Output, State
26 from yaml import load, FullLoader, YAMLError
27 from copy import deepcopy
28 from ast import literal_eval
29
30 from ..data.data import Data
31 from ..data.url_processing import url_decode, url_encode
32 from .graphs import graph_iterative, table_comparison, get_short_version
33
34
35 class Layout:
36     """
37     """
38
39     # If True, clear all inputs in control panel when button "ADD SELECTED" is
40     # pressed.
41     CLEAR_ALL_INPUTS = False
42
43     STYLE_DISABLED = {"display": "none"}
44     STYLE_ENABLED = {"display": "inherit"}
45
46     CL_ALL_DISABLED = [{
47         "label": "All",
48         "value": "all",
49         "disabled": True
50     }]
51     CL_ALL_ENABLED = [{
52         "label": "All",
53         "value": "all",
54         "disabled": False
55     }]
56
57     PLACEHOLDER = html.Nobr("")
58
59     DRIVERS = ("avf", "af-xdp", "rdma", "dpdk")
60
61     LABELS = {
62         "dpdk": "DPDK",
63         "container_memif": "LXC/DRC Container Memif",
64         "crypto": "IPSec IPv4 Routing",
65         "ip4": "IPv4 Routing",
66         "ip6": "IPv6 Routing",
67         "ip4_tunnels": "IPv4 Tunnels",
68         "l2": "L2 Ethernet Switching",
69         "srv6": "SRv6 Routing",
70         "vm_vhost": "VMs vhost-user",
71         "nfv_density-dcr_memif-chain_ipsec": "CNF Service Chains Routing IPSec",
72         "nfv_density-vm_vhost-chain_dot1qip4vxlan":"VNF Service Chains Tunnels",
73         "nfv_density-vm_vhost-chain": "VNF Service Chains Routing",
74         "nfv_density-dcr_memif-pipeline": "CNF Service Pipelines Routing",
75         "nfv_density-dcr_memif-chain": "CNF Service Chains Routing",
76     }
77
78     URL_STYLE = {
79         "background-color": "#d2ebf5",
80         "border-color": "#bce1f1",
81         "color": "#135d7c"
82     }
83
84     def __init__(self, app: Flask, releases: list, html_layout_file: str,
85         graph_layout_file: str, data_spec_file: str, tooltip_file: str) -> None:
86         """
87         """
88
89         # Inputs
90         self._app = app
91         self.releases = releases
92         self._html_layout_file = html_layout_file
93         self._graph_layout_file = graph_layout_file
94         self._data_spec_file = data_spec_file
95         self._tooltip_file = tooltip_file
96
97         # Read the data:
98         self._data = pd.DataFrame()
99         for rls in releases:
100             data_mrr = Data(self._data_spec_file, True).\
101                 read_iterative_mrr(release=rls.replace("csit", "rls"))
102             data_mrr["release"] = rls
103             data_ndrpdr = Data(self._data_spec_file, True).\
104                 read_iterative_ndrpdr(release=rls.replace("csit", "rls"))
105             data_ndrpdr["release"] = rls
106             self._data = pd.concat(
107                 [self._data, data_mrr, data_ndrpdr], ignore_index=True)
108
109         # Get structure of tests:
110         tbs = dict()
111         cols = ["job", "test_id", "test_type", "dut_version", "release"]
112         for _, row in self._data[cols].drop_duplicates().iterrows():
113             rls = row["release"]
114             ttype = row["test_type"]
115             lst_job = row["job"].split("-")
116             dut = lst_job[1]
117             d_ver = get_short_version(row["dut_version"], dut)
118             tbed = "-".join(lst_job[-2:])
119             lst_test_id = row["test_id"].split(".")
120             if dut == "dpdk":
121                 area = "dpdk"
122             else:
123                 area = "-".join(lst_test_id[3:-2])
124             suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
125                 replace("2n-", "")
126             test = lst_test_id[-1]
127             nic = suite.split("-")[0]
128             for drv in self.DRIVERS:
129                 if drv in test:
130                     driver = drv.replace("-", "_")
131                     test = test.replace(f"{drv}-", "")
132                     break
133             else:
134                 driver = "dpdk"
135             infra = "-".join((tbed, nic, driver))
136             lst_test = test.split("-")
137             framesize = lst_test[0]
138             core = lst_test[1] if lst_test[1] else "8C"
139             test = "-".join(lst_test[2: -1])
140
141             if tbs.get(rls, None) is None:
142                 tbs[rls] = dict()
143             if tbs[rls].get(dut, None) is None:
144                 tbs[rls][dut] = dict()
145             if tbs[rls][dut].get(d_ver, None) is None:
146                 tbs[rls][dut][d_ver] = dict()
147             if tbs[rls][dut][d_ver].get(infra, None) is None:
148                 tbs[rls][dut][d_ver][infra] = dict()
149             if tbs[rls][dut][d_ver][infra].get(area, None) is None:
150                 tbs[rls][dut][d_ver][infra][area] = dict()
151             if tbs[rls][dut][d_ver][infra][area].get(test, None) is None:
152                 tbs[rls][dut][d_ver][infra][area][test] = dict()
153                 tbs[rls][dut][d_ver][infra][area][test]["core"] = list()
154                 tbs[rls][dut][d_ver][infra][area][test]["frame-size"] = list()
155                 tbs[rls][dut][d_ver][infra][area][test]["test-type"] = list()
156             if core.upper() not in \
157                     tbs[rls][dut][d_ver][infra][area][test]["core"]:
158                 tbs[rls][dut][d_ver][infra][area][test]["core"].append(
159                     core.upper())
160             if framesize.upper() not in \
161                         tbs[rls][dut][d_ver][infra][area][test]["frame-size"]:
162                 tbs[rls][dut][d_ver][infra][area][test]["frame-size"].append(
163                     framesize.upper())
164             if ttype == "mrr":
165                 if "MRR" not in \
166                         tbs[rls][dut][d_ver][infra][area][test]["test-type"]:
167                     tbs[rls][dut][d_ver][infra][area][test]["test-type"].append(
168                         "MRR")
169             elif ttype == "ndrpdr":
170                 if "NDR" not in \
171                         tbs[rls][dut][d_ver][infra][area][test]["test-type"]:
172                     tbs[rls][dut][d_ver][infra][area][test]["test-type"].extend(
173                         ("NDR", "PDR", ))
174         self._spec_tbs = tbs
175
176         # Read from files:
177         self._html_layout = ""
178         self._graph_layout = None
179         self._tooltips = dict()
180
181         try:
182             with open(self._html_layout_file, "r") as file_read:
183                 self._html_layout = file_read.read()
184         except IOError as err:
185             raise RuntimeError(
186                 f"Not possible to open the file {self._html_layout_file}\n{err}"
187             )
188
189         try:
190             with open(self._graph_layout_file, "r") as file_read:
191                 self._graph_layout = load(file_read, Loader=FullLoader)
192         except IOError as err:
193             raise RuntimeError(
194                 f"Not possible to open the file {self._graph_layout_file}\n"
195                 f"{err}"
196             )
197         except YAMLError as err:
198             raise RuntimeError(
199                 f"An error occurred while parsing the specification file "
200                 f"{self._graph_layout_file}\n{err}"
201             )
202
203         try:
204             with open(self._tooltip_file, "r") as file_read:
205                 self._tooltips = load(file_read, Loader=FullLoader)
206         except IOError as err:
207             logging.warning(
208                 f"Not possible to open the file {self._tooltip_file}\n{err}"
209             )
210         except YAMLError as err:
211             logging.warning(
212                 f"An error occurred while parsing the specification file "
213                 f"{self._tooltip_file}\n{err}"
214             )
215
216         # Callbacks:
217         if self._app is not None and hasattr(self, 'callbacks'):
218             self.callbacks(self._app)
219
220     @property
221     def html_layout(self):
222         return self._html_layout
223
224     @property
225     def spec_tbs(self):
226         return self._spec_tbs
227
228     @property
229     def data(self):
230         return self._data
231
232     @property
233     def layout(self):
234         return self._graph_layout
235
236     def label(self, key: str) -> str:
237         return self.LABELS.get(key, key)
238
239     def _show_tooltip(self, id: str, title: str,
240             clipboard_id: str=None) -> list:
241         """
242         """
243         return [
244             dcc.Clipboard(target_id=clipboard_id, title="Copy URL") \
245                 if clipboard_id else str(),
246             f"{title} ",
247             dbc.Badge(
248                 id=id,
249                 children="?",
250                 pill=True,
251                 color="white",
252                 text_color="info",
253                 class_name="border ms-1",
254             ),
255             dbc.Tooltip(
256                 children=self._tooltips.get(id, str()),
257                 target=id,
258                 placement="auto"
259             )
260         ]
261
262     def add_content(self):
263         """
264         """
265         if self.html_layout and self.spec_tbs:
266             return html.Div(
267                 id="div-main",
268                 children=[
269                     dbc.Row(
270                         id="row-navbar",
271                         class_name="g-0",
272                         children=[
273                             self._add_navbar(),
274                         ]
275                     ),
276                     dcc.Loading(
277                         dbc.Offcanvas(
278                             class_name="w-50",
279                             id="offcanvas-metadata",
280                             title="Throughput And Latency",
281                             placement="end",
282                             is_open=False,
283                             children=[
284                                 dbc.Row(id="metadata-tput-lat"),
285                                 dbc.Row(id="metadata-hdrh-graph"),
286                             ]
287                         )
288                     ),
289                     dbc.Row(
290                         id="row-main",
291                         class_name="g-0",
292                         children=[
293                             dcc.Store(id="selected-tests"),
294                             dcc.Store(id="control-panel"),
295                             dcc.Location(id="url", refresh=False),
296                             self._add_ctrl_col(),
297                             self._add_plotting_col(),
298                         ]
299                     )
300                 ]
301             )
302         else:
303             return html.Div(
304                 id="div-main-error",
305                 children=[
306                     dbc.Alert(
307                         [
308                             "An Error Occured",
309                         ],
310                         color="danger",
311                     ),
312                 ]
313             )
314
315     def _add_navbar(self):
316         """Add nav element with navigation panel. It is placed on the top.
317         """
318         return dbc.NavbarSimple(
319             id="navbarsimple-main",
320             children=[
321                 dbc.NavItem(
322                     dbc.NavLink(
323                         "Iterative Test Runs",
324                         disabled=True,
325                         external_link=True,
326                         href="#"
327                     )
328                 )
329             ],
330             brand="Dashboard",
331             brand_href="/",
332             brand_external_link=True,
333             class_name="p-2",
334             fluid=True,
335         )
336
337     def _add_ctrl_col(self) -> dbc.Col:
338         """Add column with controls. It is placed on the left side.
339         """
340         return dbc.Col(
341             id="col-controls",
342             children=[
343                 self._add_ctrl_panel(),
344             ],
345         )
346
347     def _add_plotting_col(self) -> dbc.Col:
348         """Add column with plots and tables. It is placed on the right side.
349         """
350         return dbc.Col(
351             id="col-plotting-area",
352             children=[
353                 dcc.Loading(
354                     children=[
355                         dbc.Row(  # Graphs
356                             class_name="g-0 p-2",
357                             children=[
358                                 dbc.Col(
359                                     dbc.Row(  # Throughput
360                                         id="row-graph-tput",
361                                         class_name="g-0 p-2",
362                                         children=[
363                                             self.PLACEHOLDER
364                                         ]
365                                     ),
366                                     width=6
367                                 ),
368                                 dbc.Col(
369                                     dbc.Row(  # Latency
370                                         id="row-graph-lat",
371                                         class_name="g-0 p-2",
372                                         children=[
373                                             self.PLACEHOLDER
374                                         ]
375                                     ),
376                                     width=6
377                                 )
378                             ]
379                         ),
380                         dbc.Row(  # Tables
381                             id="row-table",
382                             class_name="g-0 p-2",
383                             children=[
384                                 self.PLACEHOLDER
385                             ]
386                         ),
387                         dbc.Row(  # Download
388                             id="row-btn-download",
389                             class_name="g-0 p-2",
390                             children=[
391                                 self.PLACEHOLDER
392                             ]
393                         )
394                     ]
395                 )
396             ],
397             width=9
398         )
399
400     def _add_ctrl_panel(self) -> dbc.Row:
401         """
402         """
403         return dbc.Row(
404             id="row-ctrl-panel",
405             class_name="g-0 p-2",
406             children=[
407                 dbc.Row(
408                     class_name="g-0",
409                     children=[
410                         dbc.InputGroup(
411                             [
412                                 dbc.InputGroupText(
413                                     children=self._show_tooltip(
414                                         "help-release", "CSIT Release")
415                                 ),
416                                 dbc.Select(
417                                     id="dd-ctrl-rls",
418                                     placeholder=("Select a Release..."),
419                                     options=sorted(
420                                         [
421                                             {"label": k, "value": k} \
422                                                 for k in self.spec_tbs.keys()
423                                         ],
424                                         key=lambda d: d["label"]
425                                     )
426                                 )
427                             ],
428                             class_name="mb-3",
429                             size="sm",
430                         ),
431                     ]
432                 ),
433                 dbc.Row(
434                     class_name="g-0",
435                     children=[
436                         dbc.InputGroup(
437                             [
438                                 dbc.InputGroupText(
439                                     children=self._show_tooltip(
440                                         "help-dut", "DUT")
441                                 ),
442                                 dbc.Select(
443                                     id="dd-ctrl-dut",
444                                     placeholder=(
445                                         "Select a Device under Test..."
446                                     )
447                                 )
448                             ],
449                             class_name="mb-3",
450                             size="sm",
451                         ),
452                     ]
453                 ),
454                 dbc.Row(
455                     class_name="g-0",
456                     children=[
457                         dbc.InputGroup(
458                             [
459                                 dbc.InputGroupText(
460                                     children=self._show_tooltip(
461                                         "help-dut-ver", "DUT Version")
462                                 ),
463                                 dbc.Select(
464                                     id="dd-ctrl-dutver",
465                                     placeholder=(
466                                         "Select a Version of "
467                                         "Device under Test..."
468                                     )
469                                 )
470                             ],
471                             class_name="mb-3",
472                             size="sm",
473                         ),
474                     ]
475                 ),
476                 dbc.Row(
477                     class_name="g-0",
478                     children=[
479                         dbc.InputGroup(
480                             [
481                                 dbc.InputGroupText(
482                                     children=self._show_tooltip(
483                                         "help-infra", "Infra")
484                                 ),
485                                 dbc.Select(
486                                     id="dd-ctrl-phy",
487                                     placeholder=(
488                                         "Select a Physical Test Bed "
489                                         "Topology..."
490                                     )
491                                 )
492                             ],
493                             class_name="mb-3",
494                             size="sm",
495                         ),
496                     ]
497                 ),
498                 dbc.Row(
499                     class_name="g-0",
500                     children=[
501                         dbc.InputGroup(
502                             [
503                                 dbc.InputGroupText(
504                                     children=self._show_tooltip(
505                                         "help-area", "Area")
506                                 ),
507                                 dbc.Select(
508                                     id="dd-ctrl-area",
509                                     placeholder="Select an Area...",
510                                     disabled=True,
511                                 ),
512                             ],
513                             class_name="mb-3",
514                             size="sm",
515                         ),
516                     ]
517                 ),
518                 dbc.Row(
519                     class_name="g-0",
520                     children=[
521                         dbc.InputGroup(
522                             [
523                                 dbc.InputGroupText(
524                                     children=self._show_tooltip(
525                                         "help-test", "Test")
526                                 ),
527                                 dbc.Select(
528                                     id="dd-ctrl-test",
529                                     placeholder="Select a Test...",
530                                     disabled=True,
531                                 ),
532                             ],
533                             class_name="mb-3",
534                             size="sm",
535                         ),
536                     ]
537                 ),
538                 dbc.Row(
539                     id="row-ctrl-framesize",
540                     class_name="gy-1",
541                     children=[
542                         dbc.Label(
543                             children=self._show_tooltip(
544                                 "help-framesize", "Frame Size"),
545                             class_name="p-0"
546                         ),
547                         dbc.Col(
548                             children=[
549                                 dbc.Checklist(
550                                     id="cl-ctrl-framesize-all",
551                                     options=self.CL_ALL_DISABLED,
552                                     inline=True,
553                                     switch=False
554                                 ),
555                             ],
556                             width=3
557                         ),
558                         dbc.Col(
559                             children=[
560                                 dbc.Checklist(
561                                     id="cl-ctrl-framesize",
562                                     inline=True,
563                                     switch=False
564                                 )
565                             ]
566                         )
567                     ]
568                 ),
569                 dbc.Row(
570                     id="row-ctrl-core",
571                     class_name="gy-1",
572                     children=[
573                         dbc.Label(
574                             children=self._show_tooltip(
575                                 "help-cores", "Number of Cores"),
576                             class_name="p-0"
577                         ),
578                         dbc.Col(
579                             children=[
580                                 dbc.Checklist(
581                                     id="cl-ctrl-core-all",
582                                     options=self.CL_ALL_DISABLED,
583                                     inline=False,
584                                     switch=False
585                                 )
586                             ],
587                             width=3
588                         ),
589                         dbc.Col(
590                             children=[
591                                 dbc.Checklist(
592                                     id="cl-ctrl-core",
593                                     inline=True,
594                                     switch=False
595                                 )
596                             ]
597                         )
598                     ]
599                 ),
600                 dbc.Row(
601                     id="row-ctrl-testtype",
602                     class_name="gy-1",
603                     children=[
604                         dbc.Label(
605                             children=self._show_tooltip(
606                                 "help-ttype", "Test Type"),
607                             class_name="p-0"
608                         ),
609                         dbc.Col(
610                             children=[
611                                 dbc.Checklist(
612                                     id="cl-ctrl-testtype-all",
613                                     options=self.CL_ALL_DISABLED,
614                                     inline=True,
615                                     switch=False
616                                 ),
617                             ],
618                             width=3
619                         ),
620                         dbc.Col(
621                             children=[
622                                 dbc.Checklist(
623                                     id="cl-ctrl-testtype",
624                                     inline=True,
625                                     switch=False
626                                 )
627                             ]
628                         )
629                     ]
630                 ),
631                 dbc.Row(
632                     id="row-ctrl-normalize",
633                     class_name="gy-1",
634                     children=[
635                         dbc.Label(
636                             children=self._show_tooltip(
637                                 "help-normalize", "Normalize"),
638                             class_name="p-0"
639                         ),
640                         dbc.Col(
641                             children=[
642                                 dbc.Checklist(
643                                     id="cl-ctrl-normalize",
644                                     options=[{
645                                         "value": "normalize",
646                                         "label": (
647                                             "Normalize results to CPU"
648                                             "frequency 2GHz"
649                                         )
650                                     }],
651                                     value=[],
652                                     inline=True,
653                                     switch=False
654                                 ),
655                             ]
656                         )
657                     ]
658                 ),
659                 dbc.Row(
660                     class_name="gy-1 p-0",
661                     children=[
662                         dbc.ButtonGroup(
663                             [
664                                 dbc.Button(
665                                     id="btn-ctrl-add",
666                                     children="Add Selected",
667                                     class_name="me-1",
668                                     color="info"
669                                 )
670                             ],
671                             size="md",
672                         )
673                     ]
674                 ),
675                 dbc.Row(
676                     id="row-card-sel-tests",
677                     class_name="gy-1",
678                     style=self.STYLE_DISABLED,
679                     children=[
680                         dbc.Label(
681                             "Selected tests",
682                             class_name="p-0"
683                         ),
684                         dbc.Checklist(
685                             class_name="overflow-auto",
686                             id="cl-selected",
687                             options=[],
688                             inline=False,
689                             style={"max-height": "20em"},
690                         )
691                     ],
692                 ),
693                 dbc.Row(
694                     id="row-btns-sel-tests",
695                     style=self.STYLE_DISABLED,
696                     children=[
697                         dbc.ButtonGroup(
698                             class_name="gy-2",
699                             children=[
700                                 dbc.Button(
701                                     id="btn-sel-remove",
702                                     children="Remove Selected",
703                                     class_name="w-100 me-1",
704                                     color="info",
705                                     disabled=False
706                                 ),
707                                 dbc.Button(
708                                     id="btn-sel-remove-all",
709                                     children="Remove All",
710                                     class_name="w-100 me-1",
711                                     color="info",
712                                     disabled=False
713                                 ),
714                             ],
715                             size="md",
716                         )
717                     ]
718                 ),
719             ]
720         )
721
722     class ControlPanel:
723         def __init__(self, panel: dict) -> None:
724
725             CL_ALL_DISABLED = [{
726                 "label": "All",
727                 "value": "all",
728                 "disabled": True
729             }]
730
731             # Defines also the order of keys
732             self._defaults = {
733                 "dd-rls-value": str(),
734                 "dd-dut-options": list(),
735                 "dd-dut-disabled": True,
736                 "dd-dut-value": str(),
737                 "dd-dutver-options": list(),
738                 "dd-dutver-disabled": True,
739                 "dd-dutver-value": str(),
740                 "dd-phy-options": list(),
741                 "dd-phy-disabled": True,
742                 "dd-phy-value": str(),
743                 "dd-area-options": list(),
744                 "dd-area-disabled": True,
745                 "dd-area-value": str(),
746                 "dd-test-options": list(),
747                 "dd-test-disabled": True,
748                 "dd-test-value": str(),
749                 "cl-core-options": list(),
750                 "cl-core-value": list(),
751                 "cl-core-all-value": list(),
752                 "cl-core-all-options": CL_ALL_DISABLED,
753                 "cl-framesize-options": list(),
754                 "cl-framesize-value": list(),
755                 "cl-framesize-all-value": list(),
756                 "cl-framesize-all-options": CL_ALL_DISABLED,
757                 "cl-testtype-options": list(),
758                 "cl-testtype-value": list(),
759                 "cl-testtype-all-value": list(),
760                 "cl-testtype-all-options": CL_ALL_DISABLED,
761                 "btn-add-disabled": True,
762                 "cl-normalize-value": list(),
763                 "cl-selected-options": list()
764             }
765
766             self._panel = deepcopy(self._defaults)
767             if panel:
768                 for key in self._defaults:
769                     self._panel[key] = panel[key]
770
771         @property
772         def defaults(self) -> dict:
773             return self._defaults
774
775         @property
776         def panel(self) -> dict:
777             return self._panel
778
779         def set(self, kwargs: dict) -> None:
780             for key, val in kwargs.items():
781                 if key in self._panel:
782                     self._panel[key] = val
783                 else:
784                     raise KeyError(f"The key {key} is not defined.")
785
786         def get(self, key: str) -> any:
787             return self._panel[key]
788
789         def values(self) -> tuple:
790             return tuple(self._panel.values())
791
792     @staticmethod
793     def _sync_checklists(opt: list, sel: list, all: list, id: str) -> tuple:
794         """
795         """
796         options = {v["value"] for v in opt}
797         if id =="all":
798             sel = list(options) if all else list()
799         else:
800             all = ["all", ] if set(sel) == options else list()
801         return sel, all
802
803     @staticmethod
804     def _list_tests(selection: dict) -> list:
805         """Display selected tests with checkboxes
806         """
807         if selection:
808             return [{"label": v["id"], "value": v["id"]} for v in selection]
809         else:
810             return list()
811
812     def callbacks(self, app):
813
814         def _generate_plotting_area(figs: tuple, table: pd.DataFrame,
815                 url: str) -> tuple:
816             """
817             """
818
819             (fig_tput, fig_lat) = figs
820
821             row_fig_tput = self.PLACEHOLDER
822             row_fig_lat = self.PLACEHOLDER
823             row_table = self.PLACEHOLDER
824             row_btn_dwnld = self.PLACEHOLDER
825
826             if fig_tput:
827                 row_fig_tput = [
828                     dcc.Graph(
829                         id={"type": "graph", "index": "tput"},
830                         figure=fig_tput
831                     )
832                 ]
833                 row_btn_dwnld = [
834                     dbc.Col(  # Download
835                         width=2,
836                         children=[
837                             dcc.Loading(children=[
838                                 dbc.Button(
839                                     id="btn-download-data",
840                                     children=self._show_tooltip(
841                                         "help-download", "Download Data"),
842                                     class_name="me-1",
843                                     color="info"
844                                 ),
845                                 dcc.Download(id="download-data")
846                             ]),
847                         ]
848                     ),
849                     dbc.Col(  # Show URL
850                         width=10,
851                         children=[
852                             dbc.InputGroup(
853                                 class_name="me-1",
854                                 children=[
855                                     dbc.InputGroupText(
856                                         style=self.URL_STYLE,
857                                         children=self._show_tooltip(
858                                             "help-url", "URL", "input-url")
859                                     ),
860                                     dbc.Input(
861                                         id="input-url",
862                                         readonly=True,
863                                         type="url",
864                                         style=self.URL_STYLE,
865                                         value=url
866                                     )
867                                 ]
868                             )
869                         ]
870                     )
871                 ]
872             if fig_lat:
873                 row_fig_lat = [
874                     dcc.Graph(
875                         id={"type": "graph", "index": "lat"},
876                         figure=fig_lat
877                     )
878                 ]
879             if not table.empty:
880                 row_table = [
881                     dbc.Table.from_dataframe(
882                         table,
883                         id={"type": "table", "index": "compare"},
884                         striped=True,
885                         bordered=True,
886                         hover=True
887                     )
888                 ]
889
890             return row_fig_tput, row_fig_lat, row_table, row_btn_dwnld
891
892         @app.callback(
893             Output("control-panel", "data"),  # Store
894             Output("selected-tests", "data"),  # Store
895             Output("row-graph-tput", "children"),
896             Output("row-graph-lat", "children"),
897             Output("row-table", "children"),
898             Output("row-btn-download", "children"),
899             Output("row-card-sel-tests", "style"),
900             Output("row-btns-sel-tests", "style"),
901             Output("dd-ctrl-rls", "value"),
902             Output("dd-ctrl-dut", "options"),
903             Output("dd-ctrl-dut", "disabled"),
904             Output("dd-ctrl-dut", "value"),
905             Output("dd-ctrl-dutver", "options"),
906             Output("dd-ctrl-dutver", "disabled"),
907             Output("dd-ctrl-dutver", "value"),
908             Output("dd-ctrl-phy", "options"),
909             Output("dd-ctrl-phy", "disabled"),
910             Output("dd-ctrl-phy", "value"),
911             Output("dd-ctrl-area", "options"),
912             Output("dd-ctrl-area", "disabled"),
913             Output("dd-ctrl-area", "value"),
914             Output("dd-ctrl-test", "options"),
915             Output("dd-ctrl-test", "disabled"),
916             Output("dd-ctrl-test", "value"),
917             Output("cl-ctrl-core", "options"),
918             Output("cl-ctrl-core", "value"),
919             Output("cl-ctrl-core-all", "value"),
920             Output("cl-ctrl-core-all", "options"),
921             Output("cl-ctrl-framesize", "options"),
922             Output("cl-ctrl-framesize", "value"),
923             Output("cl-ctrl-framesize-all", "value"),
924             Output("cl-ctrl-framesize-all", "options"),
925             Output("cl-ctrl-testtype", "options"),
926             Output("cl-ctrl-testtype", "value"),
927             Output("cl-ctrl-testtype-all", "value"),
928             Output("cl-ctrl-testtype-all", "options"),
929             Output("btn-ctrl-add", "disabled"),
930             Output("cl-ctrl-normalize", "value"),
931             Output("cl-selected", "options"),  # User selection
932             State("control-panel", "data"),  # Store
933             State("selected-tests", "data"),  # Store
934             State("cl-selected", "value"),  # User selection
935             Input("dd-ctrl-rls", "value"),
936             Input("dd-ctrl-dut", "value"),
937             Input("dd-ctrl-dutver", "value"),
938             Input("dd-ctrl-phy", "value"),
939             Input("dd-ctrl-area", "value"),
940             Input("dd-ctrl-test", "value"),
941             Input("cl-ctrl-core", "value"),
942             Input("cl-ctrl-core-all", "value"),
943             Input("cl-ctrl-framesize", "value"),
944             Input("cl-ctrl-framesize-all", "value"),
945             Input("cl-ctrl-testtype", "value"),
946             Input("cl-ctrl-testtype-all", "value"),
947             Input("cl-ctrl-normalize", "value"),
948             Input("btn-ctrl-add", "n_clicks"),
949             Input("btn-sel-remove", "n_clicks"),
950             Input("btn-sel-remove-all", "n_clicks"),
951             Input("url", "href")
952         )
953         def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
954             dd_rls: str, dd_dut: str, dd_dutver: str, dd_phy: str, dd_area: str,
955             dd_test: str, cl_core: list, cl_core_all: list, cl_framesize: list,
956             cl_framesize_all: list, cl_testtype: list, cl_testtype_all: list,
957             cl_normalize: list, btn_add: int, btn_remove: int,
958             btn_remove_all: int, href: str) -> tuple:
959             """
960             """
961
962             def _gen_new_url(parsed_url: dict, store_sel: list) -> str:
963
964                 if parsed_url:
965                     new_url = url_encode({
966                         "scheme": parsed_url["scheme"],
967                         "netloc": parsed_url["netloc"],
968                         "path": parsed_url["path"],
969                         "params": {
970                             "store_sel": store_sel,
971                         }
972                     })
973                 else:
974                     new_url = str()
975                 return new_url
976
977             ctrl_panel = self.ControlPanel(cp_data)
978
979             # Parse the url:
980             parsed_url = url_decode(href)
981
982             row_fig_tput = no_update
983             row_fig_lat = no_update
984             row_table = no_update
985             row_btn_dwnld = no_update
986             row_card_sel_tests = no_update
987             row_btns_sel_tests = no_update
988
989             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
990
991             if trigger_id == "dd-ctrl-rls":
992                 try:
993                     rls = self.spec_tbs[dd_rls]
994                     options = sorted(
995                         [{"label": v, "value": v} for v in rls.keys()],
996                         key=lambda d: d["label"]
997                     )
998                     disabled = False
999                 except KeyError:
1000                     options = list()
1001                     disabled = True
1002                 ctrl_panel.set({
1003                     "dd-rls-value": dd_rls,
1004                     "dd-dut-value": str(),
1005                     "dd-dut-options": options,
1006                     "dd-dut-disabled": disabled,
1007                     "dd-dutver-value": str(),
1008                     "dd-dutver-options": list(),
1009                     "dd-dutver-disabled": True,
1010                     "dd-phy-value": str(),
1011                     "dd-phy-options": list(),
1012                     "dd-phy-disabled": True,
1013                     "dd-area-value": str(),
1014                     "dd-area-options": list(),
1015                     "dd-area-disabled": True,
1016                     "dd-test-value": str(),
1017                     "dd-test-options": list(),
1018                     "dd-test-disabled": True,
1019                     "cl-core-options": list(),
1020                     "cl-core-value": list(),
1021                     "cl-core-all-value": list(),
1022                     "cl-core-all-options": self.CL_ALL_DISABLED,
1023                     "cl-framesize-options": list(),
1024                     "cl-framesize-value": list(),
1025                     "cl-framesize-all-value": list(),
1026                     "cl-framesize-all-options": self.CL_ALL_DISABLED,
1027                     "cl-testtype-options": list(),
1028                     "cl-testtype-value": list(),
1029                     "cl-testtype-all-value": list(),
1030                     "cl-testtype-all-options": self.CL_ALL_DISABLED
1031                 })
1032             elif trigger_id == "dd-ctrl-dut":
1033                 try:
1034                     rls = ctrl_panel.get("dd-rls-value")
1035                     dut = self.spec_tbs[rls][dd_dut]
1036                     options = sorted(
1037                         [{"label": v, "value": v} for v in dut.keys()],
1038                         key=lambda d: d["label"]
1039                     )
1040                     disabled = False
1041                 except KeyError:
1042                     options = list()
1043                     disabled = True
1044                 ctrl_panel.set({
1045                     "dd-dut-value": dd_dut,
1046                     "dd-dutver-value": str(),
1047                     "dd-dutver-options": options,
1048                     "dd-dutver-disabled": disabled,
1049                     "dd-phy-value": str(),
1050                     "dd-phy-options": list(),
1051                     "dd-phy-disabled": True,
1052                     "dd-area-value": str(),
1053                     "dd-area-options": list(),
1054                     "dd-area-disabled": True,
1055                     "dd-test-value": str(),
1056                     "dd-test-options": list(),
1057                     "dd-test-disabled": True,
1058                     "cl-core-options": list(),
1059                     "cl-core-value": list(),
1060                     "cl-core-all-value": list(),
1061                     "cl-core-all-options": self.CL_ALL_DISABLED,
1062                     "cl-framesize-options": list(),
1063                     "cl-framesize-value": list(),
1064                     "cl-framesize-all-value": list(),
1065                     "cl-framesize-all-options": self.CL_ALL_DISABLED,
1066                     "cl-testtype-options": list(),
1067                     "cl-testtype-value": list(),
1068                     "cl-testtype-all-value": list(),
1069                     "cl-testtype-all-options": self.CL_ALL_DISABLED
1070                 })
1071             elif trigger_id == "dd-ctrl-dutver":
1072                 try:
1073                     rls = ctrl_panel.get("dd-rls-value")
1074                     dut = ctrl_panel.get("dd-dut-value")
1075                     dutver = self.spec_tbs[rls][dut][dd_dutver]
1076                     options = sorted(
1077                         [{"label": v, "value": v} for v in dutver.keys()],
1078                         key=lambda d: d["label"]
1079                     )
1080                     disabled = False
1081                 except KeyError:
1082                     options = list()
1083                     disabled = True
1084                 ctrl_panel.set({
1085                     "dd-dutver-value": dd_dutver,
1086                     "dd-phy-value": str(),
1087                     "dd-phy-options": options,
1088                     "dd-phy-disabled": disabled,
1089                     "dd-area-value": str(),
1090                     "dd-area-options": list(),
1091                     "dd-area-disabled": True,
1092                     "dd-test-value": str(),
1093                     "dd-test-options": list(),
1094                     "dd-test-disabled": True,
1095                     "cl-core-options": list(),
1096                     "cl-core-value": list(),
1097                     "cl-core-all-value": list(),
1098                     "cl-core-all-options": self.CL_ALL_DISABLED,
1099                     "cl-framesize-options": list(),
1100                     "cl-framesize-value": list(),
1101                     "cl-framesize-all-value": list(),
1102                     "cl-framesize-all-options": self.CL_ALL_DISABLED,
1103                     "cl-testtype-options": list(),
1104                     "cl-testtype-value": list(),
1105                     "cl-testtype-all-value": list(),
1106                     "cl-testtype-all-options": self.CL_ALL_DISABLED
1107                 })
1108             elif trigger_id == "dd-ctrl-phy":
1109                 try:
1110                     rls = ctrl_panel.get("dd-rls-value")
1111                     dut = ctrl_panel.get("dd-dut-value")
1112                     dutver = ctrl_panel.get("dd-dutver-value")
1113                     phy = self.spec_tbs[rls][dut][dutver][dd_phy]
1114                     options = sorted(
1115                         [{"label": self.label(v), "value": v}
1116                             for v in phy.keys()],
1117                         key=lambda d: d["label"]
1118                     )
1119                     disabled = False
1120                 except KeyError:
1121                     options = list()
1122                     disabled = True
1123                 ctrl_panel.set({
1124                     "dd-phy-value": dd_phy,
1125                     "dd-area-value": str(),
1126                     "dd-area-options": options,
1127                     "dd-area-disabled": disabled,
1128                     "dd-test-value": str(),
1129                     "dd-test-options": list(),
1130                     "dd-test-disabled": True,
1131                     "cl-core-options": list(),
1132                     "cl-core-value": list(),
1133                     "cl-core-all-value": list(),
1134                     "cl-core-all-options": self.CL_ALL_DISABLED,
1135                     "cl-framesize-options": list(),
1136                     "cl-framesize-value": list(),
1137                     "cl-framesize-all-value": list(),
1138                     "cl-framesize-all-options": self.CL_ALL_DISABLED,
1139                     "cl-testtype-options": list(),
1140                     "cl-testtype-value": list(),
1141                     "cl-testtype-all-value": list(),
1142                     "cl-testtype-all-options": self.CL_ALL_DISABLED
1143                 })
1144             elif trigger_id == "dd-ctrl-area":
1145                 try:
1146                     rls = ctrl_panel.get("dd-rls-value")
1147                     dut = ctrl_panel.get("dd-dut-value")
1148                     dutver = ctrl_panel.get("dd-dutver-value")
1149                     phy = ctrl_panel.get("dd-phy-value")
1150                     area = self.spec_tbs[rls][dut][dutver][phy][dd_area]
1151                     options = sorted(
1152                         [{"label": v, "value": v} for v in area.keys()],
1153                         key=lambda d: d["label"]
1154                     )
1155                     disabled = False
1156                 except KeyError:
1157                     options = list()
1158                     disabled = True
1159                 ctrl_panel.set({
1160                     "dd-area-value": dd_area,
1161                     "dd-test-value": str(),
1162                     "dd-test-options": options,
1163                     "dd-test-disabled": disabled,
1164                     "cl-core-options": list(),
1165                     "cl-core-value": list(),
1166                     "cl-core-all-value": list(),
1167                     "cl-core-all-options": self.CL_ALL_DISABLED,
1168                     "cl-framesize-options": list(),
1169                     "cl-framesize-value": list(),
1170                     "cl-framesize-all-value": list(),
1171                     "cl-framesize-all-options": self.CL_ALL_DISABLED,
1172                     "cl-testtype-options": list(),
1173                     "cl-testtype-value": list(),
1174                     "cl-testtype-all-value": list(),
1175                     "cl-testtype-all-options": self.CL_ALL_DISABLED
1176                 })
1177             elif trigger_id == "dd-ctrl-test":
1178                 rls = ctrl_panel.get("dd-rls-value")
1179                 dut = ctrl_panel.get("dd-dut-value")
1180                 dutver = ctrl_panel.get("dd-dutver-value")
1181                 phy = ctrl_panel.get("dd-phy-value")
1182                 area = ctrl_panel.get("dd-area-value")
1183                 test = self.spec_tbs[rls][dut][dutver][phy][area][dd_test]
1184                 if dut and phy and area and dd_test:
1185                     ctrl_panel.set({
1186                         "dd-test-value": dd_test,
1187                         "cl-core-options": [{"label": v, "value": v}
1188                             for v in sorted(test["core"])],
1189                         "cl-core-value": list(),
1190                         "cl-core-all-value": list(),
1191                         "cl-core-all-options": self.CL_ALL_ENABLED,
1192                         "cl-framesize-options": [{"label": v, "value": v}
1193                             for v in sorted(test["frame-size"])],
1194                         "cl-framesize-value": list(),
1195                         "cl-framesize-all-value": list(),
1196                         "cl-framesize-all-options": self.CL_ALL_ENABLED,
1197                         "cl-testtype-options": [{"label": v, "value": v}
1198                             for v in sorted(test["test-type"])],
1199                         "cl-testtype-value": list(),
1200                         "cl-testtype-all-value": list(),
1201                         "cl-testtype-all-options": self.CL_ALL_ENABLED,
1202                     })
1203             elif trigger_id == "cl-ctrl-core":
1204                 val_sel, val_all = self._sync_checklists(
1205                     opt=ctrl_panel.get("cl-core-options"),
1206                     sel=cl_core,
1207                     all=list(),
1208                     id=""
1209                 )
1210                 ctrl_panel.set({
1211                     "cl-core-value": val_sel,
1212                     "cl-core-all-value": val_all,
1213                 })
1214             elif trigger_id == "cl-ctrl-core-all":
1215                 val_sel, val_all = self._sync_checklists(
1216                     opt = ctrl_panel.get("cl-core-options"),
1217                     sel=list(),
1218                     all=cl_core_all,
1219                     id="all"
1220                 )
1221                 ctrl_panel.set({
1222                     "cl-core-value": val_sel,
1223                     "cl-core-all-value": val_all,
1224                 })
1225             elif trigger_id == "cl-ctrl-framesize":
1226                 val_sel, val_all = self._sync_checklists(
1227                     opt = ctrl_panel.get("cl-framesize-options"),
1228                     sel=cl_framesize,
1229                     all=list(),
1230                     id=""
1231                 )
1232                 ctrl_panel.set({
1233                     "cl-framesize-value": val_sel,
1234                     "cl-framesize-all-value": val_all,
1235                 })
1236             elif trigger_id == "cl-ctrl-framesize-all":
1237                 val_sel, val_all = self._sync_checklists(
1238                     opt = ctrl_panel.get("cl-framesize-options"),
1239                     sel=list(),
1240                     all=cl_framesize_all,
1241                     id="all"
1242                 )
1243                 ctrl_panel.set({
1244                     "cl-framesize-value": val_sel,
1245                     "cl-framesize-all-value": val_all,
1246                 })
1247             elif trigger_id == "cl-ctrl-testtype":
1248                 val_sel, val_all = self._sync_checklists(
1249                     opt = ctrl_panel.get("cl-testtype-options"),
1250                     sel=cl_testtype,
1251                     all=list(),
1252                     id=""
1253                 )
1254                 ctrl_panel.set({
1255                     "cl-testtype-value": val_sel,
1256                     "cl-testtype-all-value": val_all,
1257                 })
1258             elif trigger_id == "cl-ctrl-testtype-all":
1259                 val_sel, val_all = self._sync_checklists(
1260                     opt = ctrl_panel.get("cl-testtype-options"),
1261                     sel=list(),
1262                     all=cl_testtype_all,
1263                     id="all"
1264                 )
1265                 ctrl_panel.set({
1266                     "cl-testtype-value": val_sel,
1267                     "cl-testtype-all-value": val_all,
1268                 })
1269             elif trigger_id == "btn-ctrl-add":
1270                 _ = btn_add
1271                 rls = ctrl_panel.get("dd-rls-value")
1272                 dut = ctrl_panel.get("dd-dut-value")
1273                 dutver = ctrl_panel.get("dd-dutver-value")
1274                 phy = ctrl_panel.get("dd-phy-value")
1275                 area = ctrl_panel.get("dd-area-value")
1276                 test = ctrl_panel.get("dd-test-value")
1277                 cores = ctrl_panel.get("cl-core-value")
1278                 framesizes = ctrl_panel.get("cl-framesize-value")
1279                 testtypes = ctrl_panel.get("cl-testtype-value")
1280                 # Add selected test to the list of tests in store:
1281                 if all((rls, dut, dutver, phy, area, test, cores, framesizes,
1282                         testtypes)):
1283                     if store_sel is None:
1284                         store_sel = list()
1285                     for core in cores:
1286                         for framesize in framesizes:
1287                             for ttype in testtypes:
1288                                 if dut == "trex":
1289                                     core = str()
1290                                 tid = "-".join((rls, dut, dutver,
1291                                     phy.replace('af_xdp', 'af-xdp'), area,
1292                                     framesize.lower(), core.lower(), test,
1293                                     ttype.lower()))
1294                                 if tid not in [itm["id"] for itm in store_sel]:
1295                                     store_sel.append({
1296                                         "id": tid,
1297                                         "rls": rls,
1298                                         "dut": dut,
1299                                         "dutver": dutver,
1300                                         "phy": phy,
1301                                         "area": area,
1302                                         "test": test,
1303                                         "framesize": framesize.lower(),
1304                                         "core": core.lower(),
1305                                         "testtype": ttype.lower()
1306                                     })
1307                     store_sel = sorted(store_sel, key=lambda d: d["id"])
1308                     row_card_sel_tests = self.STYLE_ENABLED
1309                     row_btns_sel_tests = self.STYLE_ENABLED
1310                     if self.CLEAR_ALL_INPUTS:
1311                         ctrl_panel.set(ctrl_panel.defaults)
1312                     ctrl_panel.set({
1313                         "cl-selected-options": self._list_tests(store_sel)
1314                     })
1315             elif trigger_id == "btn-sel-remove-all":
1316                 _ = btn_remove_all
1317                 row_fig_tput = self.PLACEHOLDER
1318                 row_fig_lat = self.PLACEHOLDER
1319                 row_table = self.PLACEHOLDER
1320                 row_btn_dwnld = self.PLACEHOLDER
1321                 row_card_sel_tests = self.STYLE_DISABLED
1322                 row_btns_sel_tests = self.STYLE_DISABLED
1323                 store_sel = list()
1324                 ctrl_panel.set({"cl-selected-options": list()})
1325             elif trigger_id == "btn-sel-remove":
1326                 _ = btn_remove
1327                 if list_sel:
1328                     new_store_sel = list()
1329                     for item in store_sel:
1330                         if item["id"] not in list_sel:
1331                             new_store_sel.append(item)
1332                     store_sel = new_store_sel
1333             elif trigger_id == "url":
1334                 # TODO: Add verification
1335                 url_params = parsed_url["params"]
1336                 if url_params:
1337                     store_sel = literal_eval(
1338                         url_params.get("store_sel", list())[0])
1339                     if store_sel:
1340                         row_card_sel_tests = self.STYLE_ENABLED
1341                         row_btns_sel_tests = self.STYLE_ENABLED
1342
1343             if trigger_id in ("btn-ctrl-add", "url", "btn-sel-remove-all",
1344                     "btn-sel-remove", "cl-ctrl-normalize"):
1345                 if store_sel:
1346                     row_fig_tput, row_fig_lat, row_table, row_btn_dwnld = \
1347                         _generate_plotting_area(
1348                             graph_iterative(
1349                                 self.data, store_sel, self.layout,
1350                                 bool(cl_normalize)
1351                             ),
1352                             table_comparison(
1353                                 self.data, store_sel, bool(cl_normalize)
1354                             ),
1355                             _gen_new_url(parsed_url, store_sel)
1356                         )
1357                     ctrl_panel.set({
1358                         "cl-selected-options": self._list_tests(store_sel)
1359                     })
1360                 else:
1361                     row_fig_tput = self.PLACEHOLDER
1362                     row_fig_lat = self.PLACEHOLDER
1363                     row_table = self.PLACEHOLDER
1364                     row_btn_dwnld = self.PLACEHOLDER
1365                     row_card_sel_tests = self.STYLE_DISABLED
1366                     row_btns_sel_tests = self.STYLE_DISABLED
1367                     store_sel = list()
1368                     ctrl_panel.set({"cl-selected-options": list()})
1369
1370             if ctrl_panel.get("cl-core-value") and \
1371                     ctrl_panel.get("cl-framesize-value") and \
1372                     ctrl_panel.get("cl-testtype-value"):
1373                 disabled = False
1374             else:
1375                 disabled = True
1376             ctrl_panel.set({
1377                 "btn-add-disabled": disabled,
1378                 "cl-normalize-value": cl_normalize
1379             })
1380
1381             ret_val = [
1382                 ctrl_panel.panel, store_sel,
1383                 row_fig_tput, row_fig_lat, row_table, row_btn_dwnld,
1384                 row_card_sel_tests, row_btns_sel_tests
1385             ]
1386             ret_val.extend(ctrl_panel.values())
1387             return ret_val
1388
1389         # @app.callback(
1390         #     Output("download-data", "data"),
1391         #     State("selected-tests", "data"),
1392         #     Input("btn-download-data", "n_clicks"),
1393         #     prevent_initial_call=True
1394         # )
1395         # def _download_data(store_sel, n_clicks):
1396         #     """
1397         #     """
1398
1399         #     if not n_clicks:
1400         #         raise PreventUpdate
1401
1402         #     if not store_sel:
1403         #         raise PreventUpdate
1404
1405         #     df = pd.DataFrame()
1406         #     for itm in store_sel:
1407         #         sel_data = select_trending_data(self.data, itm)
1408         #         if sel_data is None:
1409         #             continue
1410         #         df = pd.concat([df, sel_data], ignore_index=True)
1411
1412         #     return dcc.send_data_frame(df.to_csv, "trending_data.csv")