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