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