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