fcea2ab3d0d5265a2cfc83cf3dbe1c34284bd782
[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                     dbc.Offcanvas(
298                         class_name="w-75",
299                         id="offcanvas-documentation",
300                         title="Documentation",
301                         placement="end",
302                         is_open=False,
303                         children=html.Iframe(
304                             src=C.URL_DOC_REL_NOTES,
305                             width="100%",
306                             height="100%"
307                         )
308                     )
309                 ]
310             )
311         else:
312             return html.Div(
313                 id="div-main-error",
314                 children=[
315                     dbc.Alert(
316                         [
317                             "An Error Occured"
318                         ],
319                         color="danger"
320                     )
321                 ]
322             )
323
324     def _add_navbar(self):
325         """Add nav element with navigation panel. It is placed on the top.
326
327         :returns: Navigation bar.
328         :rtype: dbc.NavbarSimple
329         """
330         return dbc.NavbarSimple(
331             id="navbarsimple-main",
332             children=[
333                 dbc.NavItem(dbc.NavLink(
334                     C.REPORT_TITLE,
335                     active=True,
336                     external_link=True,
337                     href="/report"
338                 )),
339                 dbc.NavItem(dbc.NavLink(
340                     "Comparisons",
341                     external_link=True,
342                     href="/comparisons"
343                 )),
344                 dbc.NavItem(dbc.NavLink(
345                     "Coverage Data",
346                     external_link=True,
347                     href="/coverage"
348                 )),
349                 dbc.NavItem(dbc.NavLink(
350                     "Documentation",
351                     id="btn-documentation",
352                 ))
353             ],
354             brand=C.BRAND,
355             brand_href="/",
356             brand_external_link=True,
357             class_name="p-2",
358             fluid=True
359         )
360
361     def _add_ctrl_col(self) -> dbc.Col:
362         """Add column with controls. It is placed on the left side.
363
364         :returns: Column with the control panel.
365         :rtype: dbc.Col
366         """
367         return dbc.Col([
368             html.Div(
369                 children=self._add_ctrl_panel(),
370                 className="sticky-top"
371             )
372         ])
373
374     def _add_plotting_col(self) -> dbc.Col:
375         """Add column with plots. It is placed on the right side.
376
377         :returns: Column with plots.
378         :rtype: dbc.Col
379         """
380         return dbc.Col(
381             id="col-plotting-area",
382             children=[
383                 dbc.Spinner(
384                     children=[
385                         dbc.Row(
386                             id="plotting-area",
387                             class_name="g-0 p-0",
388                             children=[
389                                 C.PLACEHOLDER
390                             ]
391                         )
392                     ]
393                 )
394             ],
395             width=9
396         )
397
398     def _add_ctrl_panel(self) -> list:
399         """Add control panel.
400
401         :returns: Control panel.
402         :rtype: list
403         """
404         return [
405             dbc.Row(
406                 class_name="g-0 p-1",
407                 children=[
408                     dbc.InputGroup(
409                         [
410                             dbc.InputGroupText(
411                                 children=show_tooltip(
412                                     self._tooltips,
413                                     "help-release",
414                                     "CSIT Release"
415                                 )
416                             ),
417                             dbc.Select(
418                                 id={"type": "ctrl-dd", "index": "rls"},
419                                 placeholder="Select a Release...",
420                                 options=sorted(
421                                     [
422                                         {"label": k, "value": k} \
423                                             for k in self._spec_tbs.keys()
424                                     ],
425                                     key=lambda d: d["label"]
426                                 )
427                             )
428                         ],
429                         size="sm"
430                     )
431                 ]
432             ),
433             dbc.Row(
434                 class_name="g-0 p-1",
435                 children=[
436                     dbc.InputGroup(
437                         [
438                             dbc.InputGroupText(
439                                 children=show_tooltip(
440                                     self._tooltips,
441                                     "help-dut",
442                                     "DUT"
443                                 )
444                             ),
445                             dbc.Select(
446                                 id={"type": "ctrl-dd", "index": "dut"},
447                                 placeholder="Select a Device under Test..."
448                             )
449                         ],
450                         size="sm"
451                     )
452                 ]
453             ),
454             dbc.Row(
455                 class_name="g-0 p-1",
456                 children=[
457                     dbc.InputGroup(
458                         [
459                             dbc.InputGroupText(
460                                 children=show_tooltip(
461                                     self._tooltips,
462                                     "help-dut-ver",
463                                     "DUT Version"
464                                 )
465                             ),
466                             dbc.Select(
467                                 id={"type": "ctrl-dd", "index": "dutver"},
468                                 placeholder=\
469                                     "Select a Version of Device under Test..."
470                             )
471                         ],
472                         size="sm"
473                     )
474                 ]
475             ),
476             dbc.Row(
477                 class_name="g-0 p-1",
478                 children=[
479                     dbc.InputGroup(
480                         [
481                             dbc.InputGroupText(
482                                 children=show_tooltip(
483                                     self._tooltips,
484                                     "help-infra",
485                                     "Infra"
486                                 )
487                             ),
488                             dbc.Select(
489                                 id={"type": "ctrl-dd", "index": "phy"},
490                                 placeholder=\
491                                     "Select a Physical Test Bed Topology..."
492                             )
493                         ],
494                         size="sm"
495                     )
496                 ]
497             ),
498             dbc.Row(
499                 class_name="g-0 p-1",
500                 children=[
501                     dbc.InputGroup(
502                         [
503                             dbc.InputGroupText(
504                                 children=show_tooltip(
505                                     self._tooltips,
506                                     "help-area",
507                                     "Area"
508                                 )
509                             ),
510                             dbc.Select(
511                                 id={"type": "ctrl-dd", "index": "area"},
512                                 placeholder="Select an Area..."
513                             )
514                         ],
515                         size="sm"
516                     )
517                 ]
518             ),
519             dbc.Row(
520                 class_name="g-0 p-1",
521                 children=[
522                     dbc.InputGroup(
523                         [
524                             dbc.InputGroupText(
525                                 children=show_tooltip(
526                                     self._tooltips,
527                                     "help-test",
528                                     "Test"
529                                 )
530                             ),
531                             dbc.Select(
532                                 id={"type": "ctrl-dd", "index": "test"},
533                                 placeholder="Select a Test..."
534                             )
535                         ],
536                         size="sm"
537                     )
538                 ]
539             ),
540             dbc.Row(
541                 class_name="g-0 p-1",
542                 children=[
543                     dbc.InputGroup(
544                         [
545                             dbc.InputGroupText(
546                                 children=show_tooltip(
547                                     self._tooltips,
548                                     "help-framesize",
549                                     "Frame Size"
550                                 )
551                             ),
552                             dbc.Col(
553                                 children=[
554                                     dbc.Checklist(
555                                         id={
556                                             "type": "ctrl-cl",
557                                             "index": "frmsize-all"
558                                         },
559                                         options=C.CL_ALL_DISABLED,
560                                         inline=True,
561                                         class_name="ms-2"
562                                     )
563                                 ],
564                                 width=2
565                             ),
566                             dbc.Col(
567                                 children=[
568                                     dbc.Checklist(
569                                         id={
570                                             "type": "ctrl-cl",
571                                             "index": "frmsize"
572                                         },
573                                         inline=True
574                                     )
575                                 ]
576                             )
577                         ],
578                         style={"align-items": "center"},
579                         size="sm"
580                     )
581                 ]
582             ),
583             dbc.Row(
584                 class_name="g-0 p-1",
585                 children=[
586                     dbc.InputGroup(
587                         [
588                             dbc.InputGroupText(
589                                 children=show_tooltip(
590                                     self._tooltips,
591                                     "help-cores",
592                                     "Number of Cores"
593                                 )
594                             ),
595                             dbc.Col(
596                                 children=[
597                                     dbc.Checklist(
598                                         id={
599                                             "type": "ctrl-cl",
600                                             "index": "core-all"
601                                         },
602                                         options=C.CL_ALL_DISABLED,
603                                         inline=True,
604                                         class_name="ms-2"
605                                     )
606                                 ],
607                                 width=2
608                             ),
609                             dbc.Col(
610                                 children=[
611                                     dbc.Checklist(
612                                         id={
613                                             "type": "ctrl-cl",
614                                             "index": "core"
615                                         },
616                                         inline=True
617                                     )
618                                 ]
619                             )
620                         ],
621                         style={"align-items": "center"},
622                         size="sm"
623                     )
624                 ]
625             ),
626             dbc.Row(
627                 class_name="g-0 p-1",
628                 children=[
629                     dbc.InputGroup(
630                         [
631                             dbc.InputGroupText(
632                                 children=show_tooltip(
633                                     self._tooltips,
634                                     "help-ttype",
635                                     "Test Type"
636                                 )
637                             ),
638                             dbc.Col(
639                                 children=[
640                                     dbc.Checklist(
641                                         id={
642                                             "type": "ctrl-cl",
643                                             "index": "tsttype-all"
644                                         },
645                                         options=C.CL_ALL_DISABLED,
646                                         inline=True,
647                                         class_name="ms-2"
648                                     )
649                                 ],
650                                 width=2
651                             ),
652                             dbc.Col(
653                                 children=[
654                                     dbc.Checklist(
655                                         id={
656                                             "type": "ctrl-cl",
657                                             "index": "tsttype"
658                                         },
659                                         inline=True
660                                     )
661                                 ]
662                             )
663                         ],
664                         style={"align-items": "center"},
665                         size="sm"
666                     )
667                 ]
668             ),
669             dbc.Row(
670                 class_name="g-0 p-1",
671                 children=[
672                     dbc.InputGroup(
673                         [
674                             dbc.InputGroupText(
675                                 children=show_tooltip(
676                                     self._tooltips,
677                                     "help-normalize",
678                                     "Normalization"
679                                 )
680                             ),
681                             dbc.Col(
682                                 children=[
683                                     dbc.Checklist(
684                                         id="normalize",
685                                         options=[{
686                                             "value": "normalize",
687                                             "label": (
688                                                 "Normalize to CPU frequency "
689                                                 "2GHz"
690                                             )
691                                         }],
692                                         value=[],
693                                         inline=True,
694                                         class_name="ms-2"
695                                     )
696                                 ]
697                             )
698                         ],
699                         style={"align-items": "center"},
700                         size="sm"
701                     )
702                 ]
703             ),
704             dbc.Row(
705                 class_name="g-0 p-1",
706                 children=[
707                     dbc.Button(
708                         id={"type": "ctrl-btn", "index": "add-test"},
709                         children="Add Selected",
710                         color="info"
711                     )
712                 ]
713             ),
714             dbc.Row(
715                 id="row-card-sel-tests",
716                 class_name="g-0 p-1",
717                 style=C.STYLE_DISABLED,
718                 children=[
719                     dbc.ListGroup(
720                         class_name="overflow-auto p-0",
721                         id="lg-selected",
722                         children=[],
723                         style={"max-height": "20em"},
724                         flush=True
725                     )
726                 ]
727             ),
728             dbc.Row(
729                 id="row-btns-sel-tests",
730                 class_name="g-0 p-1",
731                 style=C.STYLE_DISABLED,
732                 children=[
733                     dbc.ButtonGroup(
734                         children=[
735                             dbc.Button(
736                                 id={"type": "ctrl-btn", "index": "rm-test"},
737                                 children="Remove Selected",
738                                 class_name="w-100",
739                                 color="info",
740                                 disabled=False
741                             ),
742                             dbc.Button(
743                                 id={"type": "ctrl-btn", "index": "rm-test-all"},
744                                 children="Remove All",
745                                 class_name="w-100",
746                                 color="info",
747                                 disabled=False
748                             )
749                         ]
750                     )
751                 ]
752             )
753         ]
754
755     def _get_plotting_area(
756             self,
757             tests: list,
758             normalize: bool,
759             url: str
760         ) -> list:
761         """Generate the plotting area with all its content.
762
763         :param tests: List of tests to be displayed in the graphs.
764         :param normalize: If true, the values in graphs are normalized.
765         :param url: URL to be displayed in the modal window.
766         :type tests: list
767         :type normalize: bool
768         :type url: str
769         :returns: List of rows with elements to be displayed in the plotting
770             area.
771         :rtype: list
772         """
773         if not tests:
774             return C.PLACEHOLDER
775
776         figs = graph_iterative(self._data, tests, self._graph_layout, normalize)
777
778         if not figs[0]:
779             return C.PLACEHOLDER
780
781         row_items = [
782             dbc.Col(
783                 children=dcc.Graph(
784                     id={"type": "graph", "index": "tput"},
785                     figure=figs[0]
786                 ),
787                 class_name="g-0 p-1",
788                 width=6
789             )
790         ]
791
792         if figs[1]:
793             row_items.append(
794                 dbc.Col(
795                     children=dcc.Graph(
796                         id={"type": "graph", "index": "lat"},
797                         figure=figs[1]
798                     ),
799                     class_name="g-0 p-1",
800                     width=6
801                 )
802             )
803
804         return [
805             dbc.Row(
806                 children=row_items,
807                 class_name="g-0 p-0",
808             ),
809             dbc.Row(
810                 [
811                     dbc.Col([html.Div(
812                         [
813                             dbc.Button(
814                                 id="plot-btn-url",
815                                 children="Show URL",
816                                 class_name="me-1",
817                                 color="info",
818                                 style={
819                                     "text-transform": "none",
820                                     "padding": "0rem 1rem"
821                                 }
822                             ),
823                             dbc.Modal(
824                                 [
825                                     dbc.ModalHeader(dbc.ModalTitle("URL")),
826                                     dbc.ModalBody(url)
827                                 ],
828                                 id="plot-mod-url",
829                                 size="xl",
830                                 is_open=False,
831                                 scrollable=True
832                             ),
833                             dbc.Button(
834                                 id="plot-btn-download",
835                                 children="Download Data",
836                                 class_name="me-1",
837                                 color="info",
838                                 style={
839                                     "text-transform": "none",
840                                     "padding": "0rem 1rem"
841                                 }
842                             ),
843                             dcc.Download(id="download-iterative-data")
844                         ],
845                         className=\
846                             "d-grid gap-0 d-md-flex justify-content-md-end"
847                     )])
848                 ],
849                 class_name="g-0 p-0"
850             )
851         ]
852
853     def callbacks(self, app):
854         """Callbacks for the whole application.
855
856         :param app: The application.
857         :type app: Flask
858         """
859
860         @app.callback(
861             [
862                 Output("store-control-panel", "data"),
863                 Output("store-selected-tests", "data"),
864                 Output("plotting-area", "children"),
865                 Output("row-card-sel-tests", "style"),
866                 Output("row-btns-sel-tests", "style"),
867                 Output("lg-selected", "children"),
868
869                 Output({"type": "ctrl-dd", "index": "rls"}, "value"),
870                 Output({"type": "ctrl-dd", "index": "dut"}, "options"),
871                 Output({"type": "ctrl-dd", "index": "dut"}, "disabled"),
872                 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
873                 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
874                 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
875                 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
876                 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
877                 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
878                 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
879                 Output({"type": "ctrl-dd", "index": "area"}, "options"),
880                 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
881                 Output({"type": "ctrl-dd", "index": "area"}, "value"),
882                 Output({"type": "ctrl-dd", "index": "test"}, "options"),
883                 Output({"type": "ctrl-dd", "index": "test"}, "disabled"),
884                 Output({"type": "ctrl-dd", "index": "test"}, "value"),
885                 Output({"type": "ctrl-cl", "index": "core"}, "options"),
886                 Output({"type": "ctrl-cl", "index": "core"}, "value"),
887                 Output({"type": "ctrl-cl", "index": "core-all"}, "value"),
888                 Output({"type": "ctrl-cl", "index": "core-all"}, "options"),
889                 Output({"type": "ctrl-cl", "index": "frmsize"}, "options"),
890                 Output({"type": "ctrl-cl", "index": "frmsize"}, "value"),
891                 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"),
892                 Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"),
893                 Output({"type": "ctrl-cl", "index": "tsttype"}, "options"),
894                 Output({"type": "ctrl-cl", "index": "tsttype"}, "value"),
895                 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"),
896                 Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"),
897                 Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"),
898                 Output("normalize", "value")
899             ],
900             [
901                 State("store-control-panel", "data"),
902                 State("store-selected-tests", "data"),
903                 State({"type": "sel-cl", "index": ALL}, "value")
904             ],
905             [
906                 Input("url", "href"),
907                 Input("normalize", "value"),
908
909                 Input({"type": "ctrl-dd", "index": ALL}, "value"),
910                 Input({"type": "ctrl-cl", "index": ALL}, "value"),
911                 Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
912             ]
913         )
914         def _update_application(
915                 control_panel: dict,
916                 store_sel: list,
917                 lst_sel: list,
918                 href: str,
919                 normalize: list,
920                 *_
921             ) -> tuple:
922             """Update the application when the event is detected.
923             """
924
925             ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
926             on_draw = False
927
928             # Parse the url:
929             parsed_url = url_decode(href)
930             if parsed_url:
931                 url_params = parsed_url["params"]
932             else:
933                 url_params = None
934
935             plotting_area = no_update
936             row_card_sel_tests = no_update
937             row_btns_sel_tests = no_update
938             lg_selected = no_update
939
940             trigger = Trigger(callback_context.triggered)
941
942             if trigger.type == "url" and url_params:
943                 try:
944                     store_sel = literal_eval(url_params["store_sel"][0])
945                     normalize = literal_eval(url_params["norm"][0])
946                 except (KeyError, IndexError, AttributeError):
947                     pass
948                 if store_sel:
949                     row_card_sel_tests = C.STYLE_ENABLED
950                     row_btns_sel_tests = C.STYLE_ENABLED
951                     last_test = store_sel[-1]
952                     test = self._spec_tbs[last_test["rls"]][last_test["dut"]]\
953                         [last_test["dutver"]][last_test["phy"]]\
954                             [last_test["area"]][last_test["test"]]
955                     ctrl_panel.set({
956                         "dd-rls-val": last_test["rls"],
957                         "dd-dut-val": last_test["dut"],
958                         "dd-dut-opt": generate_options(
959                             self._spec_tbs[last_test["rls"]].keys()
960                         ),
961                         "dd-dut-dis": False,
962                         "dd-dutver-val": last_test["dutver"],
963                         "dd-dutver-opt": generate_options(
964                             self._spec_tbs[last_test["rls"]]\
965                                 [last_test["dut"]].keys()
966                         ),
967                         "dd-dutver-dis": False,
968                         "dd-phy-val": last_test["phy"],
969                         "dd-phy-opt": generate_options(
970                             self._spec_tbs[last_test["rls"]][last_test["dut"]]\
971                                 [last_test["dutver"]].keys()
972                         ),
973                         "dd-phy-dis": False,
974                         "dd-area-val": last_test["area"],
975                         "dd-area-opt": [
976                             {"label": label(v), "value": v} for v in \
977                                 sorted(self._spec_tbs[last_test["rls"]]\
978                                     [last_test["dut"]][last_test["dutver"]]\
979                                         [last_test["phy"]].keys())
980                         ],
981                         "dd-area-dis": False,
982                         "dd-test-val": last_test["test"],
983                         "dd-test-opt": generate_options(
984                             self._spec_tbs[last_test["rls"]][last_test["dut"]]\
985                                 [last_test["dutver"]][last_test["phy"]]\
986                                     [last_test["area"]].keys()
987                         ),
988                         "dd-test-dis": False,
989                         "cl-core-opt": generate_options(test["core"]),
990                         "cl-core-val": [last_test["core"].upper(), ],
991                         "cl-core-all-val": list(),
992                         "cl-core-all-opt": C.CL_ALL_ENABLED,
993                         "cl-frmsize-opt": generate_options(test["frame-size"]),
994                         "cl-frmsize-val": [last_test["framesize"].upper(), ],
995                         "cl-frmsize-all-val": list(),
996                         "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
997                         "cl-tsttype-opt": generate_options(test["test-type"]),
998                         "cl-tsttype-val": [last_test["testtype"].upper(), ],
999                         "cl-tsttype-all-val": list(),
1000                         "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1001                         "cl-normalize-val": normalize,
1002                         "btn-add-dis": False
1003                     })
1004                     on_draw = True
1005             elif trigger.type == "normalize":
1006                 ctrl_panel.set({"cl-normalize-val": normalize})
1007                 on_draw = True
1008             elif trigger.type == "ctrl-dd":
1009                 if trigger.idx == "rls":
1010                     try:
1011                         options = generate_options(
1012                             self._spec_tbs[trigger.value].keys()
1013                         )
1014                         disabled = False
1015                     except KeyError:
1016                         options = list()
1017                         disabled = True
1018                     ctrl_panel.set({
1019                         "dd-rls-val": trigger.value,
1020                         "dd-dut-val": str(),
1021                         "dd-dut-opt": options,
1022                         "dd-dut-dis": disabled,
1023                         "dd-dutver-val": str(),
1024                         "dd-dutver-opt": list(),
1025                         "dd-dutver-dis": True,
1026                         "dd-phy-val": str(),
1027                         "dd-phy-opt": list(),
1028                         "dd-phy-dis": True,
1029                         "dd-area-val": str(),
1030                         "dd-area-opt": list(),
1031                         "dd-area-dis": True,
1032                         "dd-test-val": str(),
1033                         "dd-test-opt": list(),
1034                         "dd-test-dis": True,
1035                         "cl-core-opt": list(),
1036                         "cl-core-val": list(),
1037                         "cl-core-all-val": list(),
1038                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1039                         "cl-frmsize-opt": list(),
1040                         "cl-frmsize-val": list(),
1041                         "cl-frmsize-all-val": list(),
1042                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1043                         "cl-tsttype-opt": list(),
1044                         "cl-tsttype-val": list(),
1045                         "cl-tsttype-all-val": list(),
1046                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1047                         "btn-add-dis": True
1048                     })
1049                 elif trigger.idx == "dut":
1050                     try:
1051                         rls = ctrl_panel.get("dd-rls-val")
1052                         dut = self._spec_tbs[rls][trigger.value]
1053                         options = generate_options(dut.keys())
1054                         disabled = False
1055                     except KeyError:
1056                         options = list()
1057                         disabled = True
1058                     ctrl_panel.set({
1059                         "dd-dut-val": trigger.value,
1060                         "dd-dutver-val": str(),
1061                         "dd-dutver-opt": options,
1062                         "dd-dutver-dis": disabled,
1063                         "dd-phy-val": str(),
1064                         "dd-phy-opt": list(),
1065                         "dd-phy-dis": True,
1066                         "dd-area-val": str(),
1067                         "dd-area-opt": list(),
1068                         "dd-area-dis": True,
1069                         "dd-test-val": str(),
1070                         "dd-test-opt": list(),
1071                         "dd-test-dis": True,
1072                         "cl-core-opt": list(),
1073                         "cl-core-val": list(),
1074                         "cl-core-all-val": list(),
1075                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1076                         "cl-frmsize-opt": list(),
1077                         "cl-frmsize-val": list(),
1078                         "cl-frmsize-all-val": list(),
1079                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1080                         "cl-tsttype-opt": list(),
1081                         "cl-tsttype-val": list(),
1082                         "cl-tsttype-all-val": list(),
1083                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1084                         "btn-add-dis": True
1085                     })
1086                 elif trigger.idx == "dutver":
1087                     try:
1088                         rls = ctrl_panel.get("dd-rls-val")
1089                         dut = ctrl_panel.get("dd-dut-val")
1090                         dutver = self._spec_tbs[rls][dut][trigger.value]
1091                         options = generate_options(dutver.keys())
1092                         disabled = False
1093                     except KeyError:
1094                         options = list()
1095                         disabled = True
1096                     ctrl_panel.set({
1097                         "dd-dutver-val": trigger.value,
1098                         "dd-phy-val": str(),
1099                         "dd-phy-opt": options,
1100                         "dd-phy-dis": disabled,
1101                         "dd-area-val": str(),
1102                         "dd-area-opt": list(),
1103                         "dd-area-dis": True,
1104                         "dd-test-val": str(),
1105                         "dd-test-opt": list(),
1106                         "dd-test-dis": True,
1107                         "cl-core-opt": list(),
1108                         "cl-core-val": list(),
1109                         "cl-core-all-val": list(),
1110                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1111                         "cl-frmsize-opt": list(),
1112                         "cl-frmsize-val": list(),
1113                         "cl-frmsize-all-val": list(),
1114                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1115                         "cl-tsttype-opt": list(),
1116                         "cl-tsttype-val": list(),
1117                         "cl-tsttype-all-val": list(),
1118                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1119                         "btn-add-dis": True
1120                     })
1121                 elif trigger.idx == "phy":
1122                     try:
1123                         rls = ctrl_panel.get("dd-rls-val")
1124                         dut = ctrl_panel.get("dd-dut-val")
1125                         dutver = ctrl_panel.get("dd-dutver-val")
1126                         phy = self._spec_tbs[rls][dut][dutver][trigger.value]
1127                         options = [{"label": label(v), "value": v} \
1128                             for v in sorted(phy.keys())]
1129                         disabled = False
1130                     except KeyError:
1131                         options = list()
1132                         disabled = True
1133                     ctrl_panel.set({
1134                         "dd-phy-val": trigger.value,
1135                         "dd-area-val": str(),
1136                         "dd-area-opt": options,
1137                         "dd-area-dis": disabled,
1138                         "dd-test-val": str(),
1139                         "dd-test-opt": list(),
1140                         "dd-test-dis": True,
1141                         "cl-core-opt": list(),
1142                         "cl-core-val": list(),
1143                         "cl-core-all-val": list(),
1144                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1145                         "cl-frmsize-opt": list(),
1146                         "cl-frmsize-val": list(),
1147                         "cl-frmsize-all-val": list(),
1148                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1149                         "cl-tsttype-opt": list(),
1150                         "cl-tsttype-val": list(),
1151                         "cl-tsttype-all-val": list(),
1152                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1153                         "btn-add-dis": True
1154                     })
1155                 elif trigger.idx == "area":
1156                     try:
1157                         rls = ctrl_panel.get("dd-rls-val")
1158                         dut = ctrl_panel.get("dd-dut-val")
1159                         dutver = ctrl_panel.get("dd-dutver-val")
1160                         phy = ctrl_panel.get("dd-phy-val")
1161                         area = \
1162                             self._spec_tbs[rls][dut][dutver][phy][trigger.value]
1163                         options = generate_options(area.keys())
1164                         disabled = False
1165                     except KeyError:
1166                         options = list()
1167                         disabled = True
1168                     ctrl_panel.set({
1169                         "dd-area-val": trigger.value,
1170                         "dd-test-val": str(),
1171                         "dd-test-opt": options,
1172                         "dd-test-dis": disabled,
1173                         "cl-core-opt": list(),
1174                         "cl-core-val": list(),
1175                         "cl-core-all-val": list(),
1176                         "cl-core-all-opt": C.CL_ALL_DISABLED,
1177                         "cl-frmsize-opt": list(),
1178                         "cl-frmsize-val": list(),
1179                         "cl-frmsize-all-val": list(),
1180                         "cl-frmsize-all-opt": C.CL_ALL_DISABLED,
1181                         "cl-tsttype-opt": list(),
1182                         "cl-tsttype-val": list(),
1183                         "cl-tsttype-all-val": list(),
1184                         "cl-tsttype-all-opt": C.CL_ALL_DISABLED,
1185                         "btn-add-dis": True
1186                     })
1187                 elif trigger.idx == "test":
1188                     rls = ctrl_panel.get("dd-rls-val")
1189                     dut = ctrl_panel.get("dd-dut-val")
1190                     dutver = ctrl_panel.get("dd-dutver-val")
1191                     phy = ctrl_panel.get("dd-phy-val")
1192                     area = ctrl_panel.get("dd-area-val")
1193                     if all((rls, dut, dutver, phy, area, trigger.value, )):
1194                         test = self._spec_tbs[rls][dut][dutver][phy][area]\
1195                             [trigger.value]
1196                         ctrl_panel.set({
1197                             "dd-test-val": trigger.value,
1198                             "cl-core-opt": generate_options(test["core"]),
1199                             "cl-core-val": list(),
1200                             "cl-core-all-val": list(),
1201                             "cl-core-all-opt": C.CL_ALL_ENABLED,
1202                             "cl-frmsize-opt": \
1203                                 generate_options(test["frame-size"]),
1204                             "cl-frmsize-val": list(),
1205                             "cl-frmsize-all-val": list(),
1206                             "cl-frmsize-all-opt": C.CL_ALL_ENABLED,
1207                             "cl-tsttype-opt": \
1208                                 generate_options(test["test-type"]),
1209                             "cl-tsttype-val": list(),
1210                             "cl-tsttype-all-val": list(),
1211                             "cl-tsttype-all-opt": C.CL_ALL_ENABLED,
1212                             "btn-add-dis": True
1213                         })
1214             elif trigger.type == "ctrl-cl":
1215                 param = trigger.idx.split("-")[0]
1216                 if "-all" in trigger.idx:
1217                     c_sel, c_all, c_id = list(), trigger.value, "all"
1218                 else:
1219                     c_sel, c_all, c_id = trigger.value, list(), str()
1220                 val_sel, val_all = sync_checklists(
1221                     options=ctrl_panel.get(f"cl-{param}-opt"),
1222                     sel=c_sel,
1223                     all=c_all,
1224                     id=c_id
1225                 )
1226                 ctrl_panel.set({
1227                     f"cl-{param}-val": val_sel,
1228                     f"cl-{param}-all-val": val_all,
1229                 })
1230                 if all((ctrl_panel.get("cl-core-val"),
1231                         ctrl_panel.get("cl-frmsize-val"),
1232                         ctrl_panel.get("cl-tsttype-val"), )):
1233                     ctrl_panel.set({"btn-add-dis": False})
1234                 else:
1235                     ctrl_panel.set({"btn-add-dis": True})
1236             elif trigger.type == "ctrl-btn":
1237                 on_draw = True
1238                 if trigger.idx == "add-test":
1239                     rls = ctrl_panel.get("dd-rls-val")
1240                     dut = ctrl_panel.get("dd-dut-val")
1241                     dutver = ctrl_panel.get("dd-dutver-val")
1242                     phy = ctrl_panel.get("dd-phy-val")
1243                     area = ctrl_panel.get("dd-area-val")
1244                     test = ctrl_panel.get("dd-test-val")
1245                     # Add selected test to the list of tests in store:
1246                     if store_sel is None:
1247                         store_sel = list()
1248                     for core in ctrl_panel.get("cl-core-val"):
1249                         for framesize in ctrl_panel.get("cl-frmsize-val"):
1250                             for ttype in ctrl_panel.get("cl-tsttype-val"):
1251                                 if dut == "trex":
1252                                     core = str()
1253                                 tid = "-".join((
1254                                     rls,
1255                                     dut,
1256                                     dutver,
1257                                     phy.replace("af_xdp", "af-xdp"),
1258                                     area,
1259                                     framesize.lower(),
1260                                     core.lower(),
1261                                     test,
1262                                     ttype.lower()
1263                                 ))
1264                                 if tid not in [i["id"] for i 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                     if C.CLEAR_ALL_INPUTS:
1279                         ctrl_panel.set(ctrl_panel.defaults)
1280                 elif trigger.idx == "rm-test" and lst_sel:
1281                     new_store_sel = list()
1282                     for idx, item in enumerate(store_sel):
1283                         if not lst_sel[idx]:
1284                             new_store_sel.append(item)
1285                     store_sel = new_store_sel
1286                 elif trigger.idx == "rm-test-all":
1287                     store_sel = list()
1288
1289             if on_draw:
1290                 if store_sel:
1291                     lg_selected = get_list_group_items(
1292                         store_sel, "sel-cl", add_index=True
1293                     )
1294                     plotting_area = self._get_plotting_area(
1295                         store_sel,
1296                         bool(normalize),
1297                         gen_new_url(
1298                             parsed_url,
1299                             {"store_sel": store_sel, "norm": normalize}
1300                         )
1301                     )
1302                     row_card_sel_tests = C.STYLE_ENABLED
1303                     row_btns_sel_tests = C.STYLE_ENABLED
1304                 else:
1305                     plotting_area = C.PLACEHOLDER
1306                     row_card_sel_tests = C.STYLE_DISABLED
1307                     row_btns_sel_tests = C.STYLE_DISABLED
1308                     store_sel = list()
1309
1310             ret_val = [
1311                 ctrl_panel.panel,
1312                 store_sel,
1313                 plotting_area,
1314                 row_card_sel_tests,
1315                 row_btns_sel_tests,
1316                 lg_selected
1317             ]
1318             ret_val.extend(ctrl_panel.values)
1319             return ret_val
1320
1321         @app.callback(
1322             Output("plot-mod-url", "is_open"),
1323             [Input("plot-btn-url", "n_clicks")],
1324             [State("plot-mod-url", "is_open")],
1325         )
1326         def toggle_plot_mod_url(n, is_open):
1327             """Toggle the modal window with url.
1328             """
1329             if n:
1330                 return not is_open
1331             return is_open
1332
1333         @app.callback(
1334             Output("download-iterative-data", "data"),
1335             State("store-selected-tests", "data"),
1336             Input("plot-btn-download", "n_clicks"),
1337             prevent_initial_call=True
1338         )
1339         def _download_iterative_data(store_sel, _):
1340             """Download the data
1341
1342             :param store_sel: List of tests selected by user stored in the
1343                 browser.
1344             :type store_sel: list
1345             :returns: dict of data frame content (base64 encoded) and meta data
1346                 used by the Download component.
1347             :rtype: dict
1348             """
1349
1350             if not store_sel:
1351                 raise PreventUpdate
1352
1353             df = pd.DataFrame()
1354             for itm in store_sel:
1355                 sel_data = select_iterative_data(self._data, itm)
1356                 if sel_data is None:
1357                     continue
1358                 df = pd.concat([df, sel_data], ignore_index=True)
1359
1360             return dcc.send_data_frame(df.to_csv, C.REPORT_DOWNLOAD_FILE_NAME)
1361
1362         @app.callback(
1363             Output("metadata-tput-lat", "children"),
1364             Output("metadata-hdrh-graph", "children"),
1365             Output("offcanvas-metadata", "is_open"),
1366             Input({"type": "graph", "index": ALL}, "clickData"),
1367             prevent_initial_call=True
1368         )
1369         def _show_metadata_from_graphs(graph_data: dict) -> tuple:
1370             """Generates the data for the offcanvas displayed when a particular
1371             point in a graph is clicked on.
1372
1373             :param graph_data: The data from the clicked point in the graph.
1374             :type graph_data: dict
1375             :returns: The data to be displayed on the offcanvas and the
1376                 information to show the offcanvas.
1377             :rtype: tuple(list, list, bool)
1378             """
1379
1380             trigger = Trigger(callback_context.triggered)
1381
1382             try:
1383                 idx = 0 if trigger.idx == "tput" else 1
1384                 graph_data = graph_data[idx]["points"]
1385             except (IndexError, KeyError, ValueError, TypeError):
1386                 raise PreventUpdate
1387
1388             def _process_stats(data: list, param: str) -> list:
1389                 """Process statistical data provided by plot.ly box graph.
1390
1391                 :param data: Statistical data provided by plot.ly box graph.
1392                 :param param: Parameter saying if the data come from "tput" or
1393                     "lat" graph.
1394                 :type data: list
1395                 :type param: str
1396                 :returns: Listo of tuples where the first value is the
1397                     statistic's name and the secont one it's value.
1398                 :rtype: list
1399                 """
1400                 if len(data) == 7:
1401                     stats = ("max", "upper fence", "q3", "median", "q1",
1402                             "lower fence", "min")
1403                 elif len(data) == 9:
1404                     stats = ("outlier", "max", "upper fence", "q3", "median",
1405                             "q1", "lower fence", "min", "outlier")
1406                 elif len(data) == 1:
1407                     if param == "lat":
1408                         stats = ("Average Latency at 50% PDR", )
1409                     else:
1410                         stats = ("Throughput", )
1411                 else:
1412                     return list()
1413                 unit = " [us]" if param == "lat" else str()
1414                 return [(f"{stat}{unit}", f"{value['y']:,.0f}")
1415                         for stat, value in zip(stats, data)]
1416
1417             graph = list()
1418             if trigger.idx == "tput":
1419                 title = "Throughput"
1420             elif trigger.idx == "lat":
1421                 title = "Latency"
1422                 if len(graph_data) == 1:
1423                     hdrh_data = graph_data[0].get("customdata", None)
1424                     if hdrh_data:
1425                         name = hdrh_data.pop("name")
1426                         graph = [dbc.Card(
1427                             class_name="gy-2 p-0",
1428                             children=[
1429                                 dbc.CardHeader(html.A(
1430                                     name,
1431                                     href=f"{C.URL_JENKINS}{name}",
1432                                     target="_blank"
1433                                 )),
1434                                 dbc.CardBody(dcc.Graph(
1435                                     id="hdrh-latency-graph",
1436                                     figure=graph_hdrh_latency(
1437                                         hdrh_data, self._graph_layout
1438                                     )
1439                                 ))
1440                             ])
1441                         ]
1442             else:
1443                 raise PreventUpdate
1444             list_group_items = list()
1445             for k, v in _process_stats(graph_data, trigger.idx):
1446                 list_group_items.append(dbc.ListGroupItem([dbc.Badge(k), v]))
1447             if trigger.idx == "tput" and len(list_group_items) == 1:
1448                 job = graph_data[0].get("customdata", "")
1449                 list_group_items.append(dbc.ListGroupItem([
1450                     dbc.Badge("csit-ref"),
1451                     html.A(job, href=f"{C.URL_JENKINS}{job}", target="_blank")
1452                 ]))
1453             metadata = [
1454                 dbc.Card(
1455                     class_name="gy-2 p-0",
1456                     children=[
1457                         dbc.CardHeader(children=[
1458                             dcc.Clipboard(
1459                                 target_id="tput-lat-metadata",
1460                                 title="Copy",
1461                                 style={"display": "inline-block"}
1462                             ),
1463                             title
1464                         ]),
1465                         dbc.CardBody(
1466                             dbc.ListGroup(list_group_items, flush=True),
1467                             id="tput-lat-metadata",
1468                             class_name="p-0"
1469                         )
1470                     ]
1471                 )
1472             ]
1473
1474             return metadata, graph, True
1475
1476         @app.callback(
1477             Output("offcanvas-documentation", "is_open"),
1478             Input("btn-documentation", "n_clicks"),
1479             State("offcanvas-documentation", "is_open")
1480         )
1481         def toggle_offcanvas_documentation(n_clicks, is_open):
1482             if n_clicks:
1483                 return not is_open
1484             return is_open