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