C-Dash: Latency in coverage tables
[csit.git] / csit.infra.dash / app / cdash / coverage / 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
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 ast import literal_eval
28
29 from ..utils.constants import Constants as C
30 from ..utils.control_panel import ControlPanel
31 from ..utils.trigger import Trigger
32 from ..utils.utils import label, gen_new_url, generate_options
33 from ..utils.url_processing import url_decode
34 from .tables import coverage_tables, select_coverage_data
35
36
37 # Control panel partameters and their default values.
38 CP_PARAMS = {
39     "rls-val": str(),
40     "dut-opt": list(),
41     "dut-dis": True,
42     "dut-val": str(),
43     "dutver-opt": list(),
44     "dutver-dis": True,
45     "dutver-val": str(),
46     "phy-opt": list(),
47     "phy-dis": True,
48     "phy-val": str(),
49     "area-opt": list(),
50     "area-dis": True,
51     "area-val": str(),
52     "show-latency": ["show_latency", ]
53 }
54
55
56 class Layout:
57     """The layout of the dash app and the callbacks.
58     """
59
60     def __init__(
61             self,
62             app: Flask,
63             data_coverage: pd.DataFrame,
64             html_layout_file: str
65         ) -> None:
66         """Initialization:
67         - save the input parameters,
68         - prepare data for the control panel,
69         - read HTML layout file,
70
71         :param app: Flask application running the dash application.
72         :param html_layout_file: Path and name of the file specifying the HTML
73             layout of the dash application.
74         :type app: Flask
75         :type html_layout_file: str
76         """
77
78         # Inputs
79         self._app = app
80         self._html_layout_file = html_layout_file
81         self._data = data_coverage
82
83         # Get structure of tests:
84         tbs = dict()
85         cols = ["job", "test_id", "dut_version", "release", ]
86         for _, row in self._data[cols].drop_duplicates().iterrows():
87             rls = row["release"]
88             lst_job = row["job"].split("-")
89             dut = lst_job[1]
90             d_ver = row["dut_version"]
91             tbed = "-".join(lst_job[-2:])
92             lst_test_id = row["test_id"].split(".")
93             if dut == "dpdk":
94                 area = "dpdk"
95             else:
96                 area = ".".join(lst_test_id[3:-2])
97             suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
98                 replace("2n-", "")
99             test = lst_test_id[-1]
100             nic = suite.split("-")[0]
101             for drv in C.DRIVERS:
102                 if drv in test:
103                     driver = drv.replace("-", "_")
104                     test = test.replace(f"{drv}-", "")
105                     break
106             else:
107                 driver = "dpdk"
108             infra = "-".join((tbed, nic, driver))
109
110             if tbs.get(rls, None) is None:
111                 tbs[rls] = dict()
112             if tbs[rls].get(dut, None) is None:
113                 tbs[rls][dut] = dict()
114             if tbs[rls][dut].get(d_ver, None) is None:
115                 tbs[rls][dut][d_ver] = dict()
116             if tbs[rls][dut][d_ver].get(infra, None) is None:
117                 tbs[rls][dut][d_ver][infra] = list()
118             if area not in tbs[rls][dut][d_ver][infra]:
119                 tbs[rls][dut][d_ver][infra].append(area)
120
121         self._spec_tbs = tbs
122
123         # Read from files:
124         self._html_layout = str()
125
126         try:
127             with open(self._html_layout_file, "r") as file_read:
128                 self._html_layout = file_read.read()
129         except IOError as err:
130             raise RuntimeError(
131                 f"Not possible to open the file {self._html_layout_file}\n{err}"
132             )
133
134         # Callbacks:
135         if self._app is not None and hasattr(self, "callbacks"):
136             self.callbacks(self._app)
137
138     @property
139     def html_layout(self):
140         return self._html_layout
141
142     def add_content(self):
143         """Top level method which generated the web page.
144
145         It generates:
146         - Store for user input data,
147         - Navigation bar,
148         - Main area with control panel and ploting area.
149
150         If no HTML layout is provided, an error message is displayed instead.
151
152         :returns: The HTML div with the whole page.
153         :rtype: html.Div
154         """
155
156         if self.html_layout and self._spec_tbs:
157             return html.Div(
158                 id="div-main",
159                 className="small",
160                 children=[
161                     dbc.Row(
162                         id="row-navbar",
163                         class_name="g-0",
164                         children=[
165                             self._add_navbar()
166                         ]
167                     ),
168                     dbc.Row(
169                         id="row-main",
170                         class_name="g-0",
171                         children=[
172                             dcc.Store(id="store-selected-tests"),
173                             dcc.Store(id="store-control-panel"),
174                             dcc.Location(id="url", refresh=False),
175                             self._add_ctrl_col(),
176                             self._add_plotting_col()
177                         ]
178                     )
179                 ]
180             )
181         else:
182             return html.Div(
183                 id="div-main-error",
184                 children=[
185                     dbc.Alert(
186                         [
187                             "An Error Occured"
188                         ],
189                         color="danger"
190                     )
191                 ]
192             )
193
194     def _add_navbar(self):
195         """Add nav element with navigation panel. It is placed on the top.
196
197         :returns: Navigation bar.
198         :rtype: dbc.NavbarSimple
199         """
200         return dbc.NavbarSimple(
201             id="navbarsimple-main",
202             children=[
203                 dbc.NavItem(
204                     dbc.NavLink(
205                         C.COVERAGE_TITLE,
206                         disabled=True,
207                         external_link=True,
208                         href="#"
209                     )
210                 )
211             ],
212             brand=C.BRAND,
213             brand_href="/",
214             brand_external_link=True,
215             class_name="p-2",
216             fluid=True
217         )
218
219     def _add_ctrl_col(self) -> dbc.Col:
220         """Add column with controls. It is placed on the left side.
221
222         :returns: Column with the control panel.
223         :rtype: dbc.Col
224         """
225         return dbc.Col([
226             html.Div(
227                 children=self._add_ctrl_panel(),
228                 className="sticky-top"
229             )
230         ])
231
232     def _add_plotting_col(self) -> dbc.Col:
233         """Add column with plots. It is placed on the right side.
234
235         :returns: Column with plots.
236         :rtype: dbc.Col
237         """
238         return dbc.Col(
239             id="col-plotting-area",
240             children=[
241                 dbc.Spinner(
242                     children=[
243                         dbc.Row(
244                             id="plotting-area",
245                             class_name="g-0 p-0",
246                             children=[
247                                 C.PLACEHOLDER
248                             ]
249                         )
250                     ]
251                 )
252             ],
253             width=9
254         )
255
256     def _add_ctrl_panel(self) -> list:
257         """Add control panel.
258
259         :returns: Control panel.
260         :rtype: list
261         """
262         return [
263             dbc.Row(
264                 class_name="g-0 p-1",
265                 children=[
266                     dbc.InputGroup(
267                         [
268                             dbc.InputGroupText("CSIT Release"),
269                             dbc.Select(
270                                 id={"type": "ctrl-dd", "index": "rls"},
271                                 placeholder="Select a Release...",
272                                 options=sorted(
273                                     [
274                                         {"label": k, "value": k} \
275                                             for k in self._spec_tbs.keys()
276                                     ],
277                                     key=lambda d: d["label"]
278                                 )
279                             )
280                         ],
281                         size="sm"
282                     )
283                 ]
284             ),
285             dbc.Row(
286                 class_name="g-0 p-1",
287                 children=[
288                     dbc.InputGroup(
289                         [
290                             dbc.InputGroupText("DUT"),
291                             dbc.Select(
292                                 id={"type": "ctrl-dd", "index": "dut"},
293                                 placeholder="Select a Device under Test..."
294                             )
295                         ],
296                         size="sm"
297                     )
298                 ]
299             ),
300             dbc.Row(
301                 class_name="g-0 p-1",
302                 children=[
303                     dbc.InputGroup(
304                         [
305                             dbc.InputGroupText("DUT Version"),
306                             dbc.Select(
307                                 id={"type": "ctrl-dd", "index": "dutver"},
308                                 placeholder=\
309                                     "Select a Version of Device under Test..."
310                             )
311                         ],
312                         size="sm"
313                     )
314                 ]
315             ),
316             dbc.Row(
317                 class_name="g-0 p-1",
318                 children=[
319                     dbc.InputGroup(
320                         [
321                             dbc.InputGroupText("Infra"),
322                             dbc.Select(
323                                 id={"type": "ctrl-dd", "index": "phy"},
324                                 placeholder=\
325                                     "Select a Physical Test Bed Topology..."
326                             )
327                         ],
328                         size="sm"
329                     )
330                 ]
331             ),
332             dbc.Row(
333                 class_name="g-0 p-1",
334                 children=[
335                     dbc.InputGroup(
336                         [
337                             dbc.InputGroupText("Area"),
338                             dbc.Select(
339                                 id={"type": "ctrl-dd", "index": "area"},
340                                 placeholder="Select an Area..."
341                             )
342                         ],
343                         size="sm"
344                     )
345                 ]
346             ),
347             dbc.Row(
348                 class_name="g-0 p-1",
349                 children=[
350                     dbc.InputGroup(
351                         [
352                             dbc.InputGroupText("Latency"),
353                             dbc.Checklist(
354                                 id="show-latency",
355                                 options=[{
356                                     "value": "show_latency",
357                                     "label": "Show Latency"
358                                 }],
359                                 value=["show_latency"],
360                                 inline=True,
361                                 class_name="ms-2"
362                             )
363                         ],
364                         style={"align-items": "center"},
365                         size="sm"
366                     )
367                 ]
368             )
369         ]
370
371     def _get_plotting_area(
372             self,
373             selected: dict,
374             url: str,
375             show_latency: bool
376         ) -> list:
377         """Generate the plotting area with all its content.
378
379         :param selected: Selected parameters of tests.
380         :param url: URL to be displayed in the modal window.
381         :param show_latency: If True, latency is displayed in the tables.
382         :type selected: dict
383         :type url: str
384         :type show_latency: bool
385         :returns: List of rows with elements to be displayed in the plotting
386             area.
387         :rtype: list
388         """
389         if not selected:
390             return C.PLACEHOLDER
391
392         return [
393             dbc.Row(
394                 children=coverage_tables(self._data, selected, show_latency),
395                 class_name="g-0 p-0",
396             ),
397             dbc.Row(
398                 children=C.PLACEHOLDER,
399                 class_name="g-0 p-1"
400             ),
401             dbc.Row(
402                 [
403                     dbc.Col([html.Div(
404                         [
405                             dbc.Button(
406                                 id="plot-btn-url",
407                                 children="Show URL",
408                                 class_name="me-1",
409                                 color="info",
410                                 style={
411                                     "text-transform": "none",
412                                     "padding": "0rem 1rem"
413                                 }
414                             ),
415                             dbc.Modal(
416                                 [
417                                     dbc.ModalHeader(dbc.ModalTitle("URL")),
418                                     dbc.ModalBody(url)
419                                 ],
420                                 id="plot-mod-url",
421                                 size="xl",
422                                 is_open=False,
423                                 scrollable=True
424                             ),
425                             dbc.Button(
426                                 id="plot-btn-download",
427                                 children="Download Data",
428                                 class_name="me-1",
429                                 color="info",
430                                 style={
431                                     "text-transform": "none",
432                                     "padding": "0rem 1rem"
433                                 }
434                             ),
435                             dcc.Download(id="download-iterative-data")
436                         ],
437                         className=\
438                             "d-grid gap-0 d-md-flex justify-content-md-end"
439                     )])
440                 ],
441                 class_name="g-0 p-0"
442             ),
443             dbc.Row(
444                 children=C.PLACEHOLDER,
445                 class_name="g-0 p-1"
446             )
447         ]
448
449     def callbacks(self, app):
450         """Callbacks for the whole application.
451
452         :param app: The application.
453         :type app: Flask
454         """
455
456         @app.callback(
457             [
458                 Output("store-control-panel", "data"),
459                 Output("store-selected-tests", "data"),
460                 Output("plotting-area", "children"),
461                 Output({"type": "ctrl-dd", "index": "rls"}, "value"),
462                 Output({"type": "ctrl-dd", "index": "dut"}, "options"),
463                 Output({"type": "ctrl-dd", "index": "dut"}, "disabled"),
464                 Output({"type": "ctrl-dd", "index": "dut"}, "value"),
465                 Output({"type": "ctrl-dd", "index": "dutver"}, "options"),
466                 Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"),
467                 Output({"type": "ctrl-dd", "index": "dutver"}, "value"),
468                 Output({"type": "ctrl-dd", "index": "phy"}, "options"),
469                 Output({"type": "ctrl-dd", "index": "phy"}, "disabled"),
470                 Output({"type": "ctrl-dd", "index": "phy"}, "value"),
471                 Output({"type": "ctrl-dd", "index": "area"}, "options"),
472                 Output({"type": "ctrl-dd", "index": "area"}, "disabled"),
473                 Output({"type": "ctrl-dd", "index": "area"}, "value"),
474                 Output("show-latency", "value"),
475             ],
476             [
477                 State("store-control-panel", "data"),
478                 State("store-selected-tests", "data")
479             ],
480             [
481                 Input("url", "href"),
482                 Input("show-latency", "value"),
483                 Input({"type": "ctrl-dd", "index": ALL}, "value")
484             ]
485         )
486         def _update_application(
487                 control_panel: dict,
488                 selected: dict,
489                 href: str,
490                 show_latency: list,
491                 *_
492             ) -> tuple:
493             """Update the application when the event is detected.
494             """
495
496             ctrl_panel = ControlPanel(CP_PARAMS, control_panel)
497             plotting_area = no_update
498             on_draw = False
499             if selected is None:
500                 selected = dict()
501
502             # Parse the url:
503             parsed_url = url_decode(href)
504             if parsed_url:
505                 url_params = parsed_url["params"]
506             else:
507                 url_params = None
508
509             trigger = Trigger(callback_context.triggered)
510
511             if trigger.type == "url" and url_params:
512                 try:
513                     show_latency = literal_eval(url_params["show_latency"][0])
514                     selected = literal_eval(url_params["selection"][0])
515                 except (KeyError, IndexError, AttributeError):
516                     pass
517                 if selected:
518                     ctrl_panel.set({
519                         "rls-val": selected["rls"],
520                         "dut-val": selected["dut"],
521                         "dut-opt": generate_options(
522                             self._spec_tbs[selected["rls"]].keys()
523                         ),
524                         "dut-dis": False,
525                         "dutver-val": selected["dutver"],
526                         "dutver-opt": generate_options(
527                             self._spec_tbs[selected["rls"]]\
528                                 [selected["dut"]].keys()
529                         ),
530                         "dutver-dis": False,
531                         "phy-val": selected["phy"],
532                         "phy-opt": generate_options(
533                             self._spec_tbs[selected["rls"]][selected["dut"]]\
534                                 [selected["dutver"]].keys()
535                         ),
536                         "phy-dis": False,
537                         "area-val": selected["area"],
538                         "area-opt": [
539                             {"label": label(v), "value": v} for v in sorted(
540                                 self._spec_tbs[selected["rls"]]\
541                                     [selected["dut"]][selected["dutver"]]\
542                                         [selected["phy"]]
543                             )
544                         ],
545                         "area-dis": False,
546                         "show-latency": show_latency
547                     })
548                     on_draw = True
549             elif trigger.type == "show-latency":
550                 ctrl_panel.set({"show-latency": show_latency})
551                 on_draw = True
552             elif trigger.type == "ctrl-dd":
553                 if trigger.idx == "rls":
554                     try:
555                         options = generate_options(
556                             self._spec_tbs[trigger.value].keys()
557                         )
558                         disabled = False
559                     except KeyError:
560                         options = list()
561                         disabled = True
562                     ctrl_panel.set({
563                         "rls-val": trigger.value,
564                         "dut-val": str(),
565                         "dut-opt": options,
566                         "dut-dis": disabled,
567                         "dutver-val": str(),
568                         "dutver-opt": list(),
569                         "dutver-dis": True,
570                         "phy-val": str(),
571                         "phy-opt": list(),
572                         "phy-dis": True,
573                         "area-val": str(),
574                         "area-opt": list(),
575                         "area-dis": True
576                     })
577                 elif trigger.idx == "dut":
578                     try:
579                         rls = ctrl_panel.get("rls-val")
580                         dut = self._spec_tbs[rls][trigger.value]
581                         options = generate_options(dut.keys())
582                         disabled = False
583                     except KeyError:
584                         options = list()
585                         disabled = True
586                     ctrl_panel.set({
587                         "dut-val": trigger.value,
588                         "dutver-val": str(),
589                         "dutver-opt": options,
590                         "dutver-dis": disabled,
591                         "phy-val": str(),
592                         "phy-opt": list(),
593                         "phy-dis": True,
594                         "area-val": str(),
595                         "area-opt": list(),
596                         "area-dis": True
597                     })
598                 elif trigger.idx == "dutver":
599                     try:
600                         rls = ctrl_panel.get("rls-val")
601                         dut = ctrl_panel.get("dut-val")
602                         dutver = self._spec_tbs[rls][dut][trigger.value]
603                         options = generate_options(dutver.keys())
604                         disabled = False
605                     except KeyError:
606                         options = list()
607                         disabled = True
608                     ctrl_panel.set({
609                         "dutver-val": trigger.value,
610                         "phy-val": str(),
611                         "phy-opt": options,
612                         "phy-dis": disabled,
613                         "area-val": str(),
614                         "area-opt": list(),
615                         "area-dis": True
616                     })
617                 elif trigger.idx == "phy":
618                     try:
619                         rls = ctrl_panel.get("rls-val")
620                         dut = ctrl_panel.get("dut-val")
621                         dutver = ctrl_panel.get("dutver-val")
622                         phy = self._spec_tbs[rls][dut][dutver][trigger.value]
623                         options = [
624                             {"label": label(v), "value": v} for v in sorted(phy)
625                         ]
626                         disabled = False
627                     except KeyError:
628                         options = list()
629                         disabled = True
630                     ctrl_panel.set({
631                         "phy-val": trigger.value,
632                         "area-val": str(),
633                         "area-opt": options,
634                         "area-dis": disabled
635                     })
636                 elif trigger.idx == "area":
637                     ctrl_panel.set({"area-val": trigger.value})
638                     selected = {
639                         "rls": ctrl_panel.get("rls-val"),
640                         "dut": ctrl_panel.get("dut-val"),
641                         "dutver": ctrl_panel.get("dutver-val"),
642                         "phy": ctrl_panel.get("phy-val"),
643                         "area": ctrl_panel.get("area-val"),
644                     }
645                     on_draw = True
646
647             if on_draw:
648                 if selected:
649                     plotting_area = self._get_plotting_area(
650                         selected,
651                         gen_new_url(
652                             parsed_url,
653                             {
654                                 "selection": selected,
655                                 "show_latency": show_latency
656                             }
657                         ),
658                         show_latency=bool(show_latency)
659                     )
660                 else:
661                     plotting_area = C.PLACEHOLDER
662                     selected = dict()
663
664             ret_val = [
665                 ctrl_panel.panel,
666                 selected,
667                 plotting_area,
668             ]
669             ret_val.extend(ctrl_panel.values)
670             return ret_val
671
672         @app.callback(
673             Output("plot-mod-url", "is_open"),
674             [Input("plot-btn-url", "n_clicks")],
675             [State("plot-mod-url", "is_open")],
676         )
677         def toggle_plot_mod_url(n, is_open):
678             """Toggle the modal window with url.
679             """
680             if n:
681                 return not is_open
682             return is_open
683
684         @app.callback(
685             Output("download-iterative-data", "data"),
686             State("store-selected-tests", "data"),
687             State("show-latency", "value"),
688             Input("plot-btn-download", "n_clicks"),
689             prevent_initial_call=True
690         )
691         def _download_coverage_data(selection, show_latency, _):
692             """Download the data
693
694             :param selection: List of tests selected by user stored in the
695                 browser.
696             :param show_latency: If True, latency is displayed in the tables.
697             :type selection: dict
698             :type show_latency: bool
699             :returns: dict of data frame content (base64 encoded) and meta data
700                 used by the Download component.
701             :rtype: dict
702             """
703
704             if not selection:
705                 raise PreventUpdate
706
707             df = select_coverage_data(
708                 self._data,
709                 selection,
710                 csv=True,
711                 show_latency=bool(show_latency)
712             )
713
714             return dcc.send_data_frame(df.to_csv, C.COVERAGE_DOWNLOAD_FILE_NAME)