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