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