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