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