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