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