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