Update VPP_STABLE_VER files
[csit.git] / resources / tools / dash / app / pal / stats / layout.py
1 # Copyright (c) 2022 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Plotly Dash HTML layout override.
15 """
16
17 import pandas as pd
18 import dash_bootstrap_components as dbc
19
20 from flask import Flask
21 from dash import dcc
22 from dash import html
23 from dash import callback_context, no_update
24 from dash import Input, Output, State
25 from dash.exceptions import PreventUpdate
26 from yaml import load, FullLoader, YAMLError
27 from datetime import datetime, timedelta
28 from copy import deepcopy
29
30 from ..data.data import Data
31 from .graphs import graph_statistics
32
33
34 class Layout:
35     """
36     """
37
38     DEFAULT_JOB = "csit-vpp-perf-mrr-daily-master-2n-icx"
39
40     def __init__(self, app: Flask, html_layout_file: str, spec_file: str,
41         graph_layout_file: str, data_spec_file: str,
42         time_period: int=None) -> None:
43         """
44         """
45
46         # Inputs
47         self._app = app
48         self._html_layout_file = html_layout_file
49         self._spec_file = spec_file
50         self._graph_layout_file = graph_layout_file
51         self._data_spec_file = data_spec_file
52         self._time_period = time_period
53
54         # Read the data:
55         data_stats, data_mrr, data_ndrpdr = Data(
56             data_spec_file=self._data_spec_file,
57             debug=True
58         ).read_stats(days=self._time_period)
59
60         df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
61
62         # Pre-process the data:
63         data_stats = data_stats[~data_stats.job.str.contains("-verify-")]
64         data_stats = data_stats[~data_stats.job.str.contains("-coverage-")]
65         data_stats = data_stats[~data_stats.job.str.contains("-iterative-")]
66         data_stats = data_stats[["job", "build", "start_time", "duration"]]
67
68         data_time_period = \
69             (datetime.utcnow() - data_stats["start_time"].min()).days
70         if self._time_period > data_time_period:
71             self._time_period = data_time_period
72
73         jobs = sorted(list(data_stats["job"].unique()))
74         job_info = {
75             "job": list(),
76             "dut": list(),
77             "ttype": list(),
78             "cadence": list(),
79             "tbed": list()
80         }
81         for job in jobs:
82             lst_job = job.split("-")
83             job_info["job"].append(job)
84             job_info["dut"].append(lst_job[1])
85             job_info["ttype"].append(lst_job[3])
86             job_info["cadence"].append(lst_job[4])
87             job_info["tbed"].append("-".join(lst_job[-2:]))
88         self.df_job_info = pd.DataFrame.from_dict(job_info)
89
90         lst_job = self.DEFAULT_JOB.split("-")
91         self._default = {
92             "job": self.DEFAULT_JOB,
93             "dut": lst_job[1],
94             "ttype": lst_job[3],
95             "cadence": lst_job[4],
96             "tbed": "-".join(lst_job[-2:]),
97             "duts": self._generate_options(self._get_duts()),
98             "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
99             "cadences": self._generate_options(self._get_cadences(
100                 lst_job[1], lst_job[3])),
101             "tbeds": self._generate_options(self._get_test_beds(
102                 lst_job[1], lst_job[3], lst_job[4]))
103         }
104
105         tst_info = {
106             "job": list(),
107             "build": list(),
108             "dut_type": list(),
109             "dut_version": list(),
110             "hosts": list(),
111             "passed": list(),
112             "failed": list()
113         }
114         for job in jobs:
115             # TODO: Add list of failed tests for each build
116             df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
117             builds = df_job["build"].unique()
118             for build in builds:
119                 df_build = df_job.loc[(df_job["build"] == build)]
120                 tst_info["job"].append(job)
121                 tst_info["build"].append(build)
122                 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
123                 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
124                 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
125                 try:
126                     passed = df_build.value_counts(subset='passed')[True]
127                 except KeyError:
128                     passed = 0
129                 try:
130                     failed = df_build.value_counts(subset='passed')[False]
131                 except KeyError:
132                     failed = 0
133                 tst_info["passed"].append(passed)
134                 tst_info["failed"].append(failed)
135
136         self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
137
138         # Read from files:
139         self._html_layout = ""
140         self._graph_layout = None
141
142         try:
143             with open(self._html_layout_file, "r") as file_read:
144                 self._html_layout = file_read.read()
145         except IOError as err:
146             raise RuntimeError(
147                 f"Not possible to open the file {self._html_layout_file}\n{err}"
148             )
149
150         try:
151             with open(self._graph_layout_file, "r") as file_read:
152                 self._graph_layout = load(file_read, Loader=FullLoader)
153         except IOError as err:
154             raise RuntimeError(
155                 f"Not possible to open the file {self._graph_layout_file}\n"
156                 f"{err}"
157             )
158         except YAMLError as err:
159             raise RuntimeError(
160                 f"An error occurred while parsing the specification file "
161                 f"{self._graph_layout_file}\n"
162                 f"{err}"
163             )
164
165         self._default_fig_passed, self._default_fig_duration = graph_statistics(
166             self.data, self._default["job"], self.layout
167         )
168
169         # Callbacks:
170         if self._app is not None and hasattr(self, 'callbacks'):
171             self.callbacks(self._app)
172
173     @property
174     def html_layout(self) -> dict:
175         return self._html_layout
176
177     @property
178     def data(self) -> pd.DataFrame:
179         return self._data
180
181     @property
182     def layout(self) -> dict:
183         return self._graph_layout
184
185     @property
186     def time_period(self) -> int:
187         return self._time_period
188
189     @property
190     def default(self) -> any:
191         return self._default
192
193     def _get_duts(self) -> list:
194         """
195         """
196         return sorted(list(self.df_job_info["dut"].unique()))
197
198     def _get_ttypes(self, dut: str) -> list:
199         """
200         """
201         return sorted(list(self.df_job_info.loc[(
202             self.df_job_info["dut"] == dut
203         )]["ttype"].unique()))
204
205     def _get_cadences(self, dut: str, ttype: str) -> list:
206         """
207         """
208         return sorted(list(self.df_job_info.loc[(
209             (self.df_job_info["dut"] == dut) &
210             (self.df_job_info["ttype"] == ttype)
211         )]["cadence"].unique()))
212
213     def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list:
214         """
215         """
216         return sorted(list(self.df_job_info.loc[(
217             (self.df_job_info["dut"] == dut) &
218             (self.df_job_info["ttype"] == ttype) &
219             (self.df_job_info["cadence"] == cadence)
220         )]["tbed"].unique()))
221
222     def _get_job(self, dut, ttype, cadence, testbed):
223         """Get the name of a job defined by dut, ttype, cadence, testbed.
224
225         Input information comes from control panel.
226         """
227         return self.df_job_info.loc[(
228             (self.df_job_info["dut"] == dut) &
229             (self.df_job_info["ttype"] == ttype) &
230             (self.df_job_info["cadence"] == cadence) &
231             (self.df_job_info["tbed"] == testbed)
232         )]["job"].item()
233
234
235     def add_content(self):
236         """
237         """
238         if self.html_layout:
239             return html.Div(
240                 id="div-main",
241                 children=[
242                     dcc.Store(
243                         id="control-panel"
244                     ),
245                     dbc.Row(
246                         id="row-navbar",
247                         class_name="g-0",
248                         children=[
249                             self._add_navbar(),
250                         ]
251                     ),
252                     dcc.Loading(
253                         dbc.Offcanvas(
254                             class_name="w-25",
255                             id="offcanvas-metadata",
256                             title="Detailed Information",
257                             placement="end",
258                             is_open=False,
259                             children=[
260                                 dbc.Row(id="row-metadata")
261                             ]
262                         )
263                     ),
264                     dbc.Row(
265                         id="row-main",
266                         class_name="g-0",
267                         children=[
268                             self._add_ctrl_col(),
269                             self._add_plotting_col(),
270                         ]
271                     )
272                 ]
273             )
274         else:
275             return html.Div(
276                 id="div-main-error",
277                 children=[
278                     dbc.Alert(
279                         [
280                             "An Error Occured",
281                         ],
282                         color="danger",
283                     ),
284                 ]
285             )
286
287     def _add_navbar(self):
288         """Add nav element with navigation panel. It is placed on the top.
289         """
290         return dbc.NavbarSimple(
291             id="navbarsimple-main",
292             children=[
293                 dbc.NavItem(
294                     dbc.NavLink(
295                         "Continuous Performance Statistics",
296                         disabled=True,
297                         external_link=True,
298                         href="#"
299                     )
300                 )
301             ],
302             brand="Dashboard",
303             brand_href="/",
304             brand_external_link=True,
305             class_name="p-2",
306             fluid=True,
307         )
308
309     def _add_ctrl_col(self) -> dbc.Col:
310         """Add column with controls. It is placed on the left side.
311         """
312         return dbc.Col(
313             id="col-controls",
314             children=[
315                 self._add_ctrl_panel(),
316             ],
317         )
318
319     def _add_plotting_col(self) -> dbc.Col:
320         """Add column with plots and tables. It is placed on the right side.
321         """
322         return dbc.Col(
323             id="col-plotting-area",
324             children=[
325                 dbc.Row(  # Passed / failed tests
326                     id="row-graph-passed",
327                     class_name="g-0 p-2",
328                     children=[
329                         dcc.Loading(children=[
330                             dcc.Graph(
331                                 id="graph-passed",
332                                 figure=self._default_fig_passed
333                             )
334                         ])
335                     ]
336                 ),
337                 dbc.Row(  # Duration
338                     id="row-graph-duration",
339                     class_name="g-0 p-2",
340                     children=[
341                         dcc.Loading(children=[
342                             dcc.Graph(
343                                 id="graph-duration",
344                                 figure=self._default_fig_duration
345                             )
346                         ])
347                     ]
348                 ),
349                 dbc.Row(  # Download
350                     id="row-btn-download",
351                     class_name="g-0 p-2",
352                     children=[
353                         dcc.Loading(children=[
354                             dbc.Button(
355                                 id="btn-download-data",
356                                 children=["Download Data"],
357                                 class_name="me-1",
358                                 color="info"
359                             ),
360                             dcc.Download(id="download-data")
361                         ])
362                     ]
363                 )
364             ],
365             width=9,
366         )
367
368     def _add_ctrl_panel(self) -> dbc.Row:
369         """
370         """
371         return dbc.Row(
372             id="row-ctrl-panel",
373             class_name="g-0",
374             children=[
375                 dbc.Row(
376                     class_name="g-0 p-2",
377                     children=[
378                         dbc.Row(
379                             class_name="gy-1",
380                             children=[
381                                 dbc.Label(
382                                     "Device under Test",
383                                     class_name="p-0"
384                                 ),
385                                 dbc.RadioItems(
386                                     id="ri-duts",
387                                     inline=True,
388                                     value=self.default["dut"],
389                                     options=self.default["duts"]
390                                 )
391                             ]
392                         ),
393                         dbc.Row(
394                             class_name="gy-1",
395                             children=[
396                                 dbc.Label(
397                                     "Test Type",
398                                     class_name="p-0"
399                                 ),
400                                 dbc.RadioItems(
401                                     id="ri-ttypes",
402                                     inline=True,
403                                     value=self.default["ttype"],
404                                     options=self.default["ttypes"]
405                                 )
406                             ]
407                         ),
408                         dbc.Row(
409                             class_name="gy-1",
410                             children=[
411                                 dbc.Label(
412                                     "Cadence",
413                                     class_name="p-0"
414                                 ),
415                                 dbc.RadioItems(
416                                     id="ri-cadences",
417                                     inline=True,
418                                     value=self.default["cadence"],
419                                     options=self.default["cadences"]
420                                 )
421                             ]
422                         ),
423                         dbc.Row(
424                             class_name="gy-1",
425                             children=[
426                                 dbc.Label(
427                                     "Test Bed",
428                                     class_name="p-0"
429                                 ),
430                                 dbc.Select(
431                                     id="dd-tbeds",
432                                     placeholder="Select a test bed...",
433                                     value=self.default["tbed"],
434                                     options=self.default["tbeds"]
435                                 )
436                             ]
437                         ),
438                         dbc.Row(
439                             class_name="gy-1",
440                             children=[
441                                 dbc.Alert(
442                                     id="al-job",
443                                     color="info",
444                                     children=self.default["job"]
445                                 )
446                             ]
447                         )
448                     ]
449                 ),
450                 dbc.Row(
451                     class_name="g-0 p-2",
452                     children=[
453                         dbc.Label("Choose the Time Period"),
454                         dcc.DatePickerRange(
455                             id="dpr-period",
456                             className="d-flex justify-content-center",
457                             min_date_allowed=\
458                                 datetime.utcnow() - timedelta(
459                                     days=self.time_period),
460                             max_date_allowed=datetime.utcnow(),
461                             initial_visible_month=datetime.utcnow(),
462                             start_date=\
463                                 datetime.utcnow() - timedelta(
464                                     days=self.time_period),
465                             end_date=datetime.utcnow(),
466                             display_format="D MMMM YY"
467                         )
468                     ]
469                 )
470             ]
471         )
472
473     class ControlPanel:
474         def __init__(self, panel: dict, default: dict) -> None:
475             self._defaults = {
476                 "ri-ttypes-options": default["ttypes"],
477                 "ri-cadences-options": default["cadences"],
478                 "dd-tbeds-options": default["tbeds"],
479                 "ri-duts-value": default["dut"],
480                 "ri-ttypes-value": default["ttype"],
481                 "ri-cadences-value": default["cadence"],
482                 "dd-tbeds-value": default["tbed"],
483                 "al-job-children": default["job"]
484             }
485             self._panel = deepcopy(self._defaults)
486             if panel:
487                 for key in self._defaults:
488                     self._panel[key] = panel[key]
489
490         def set(self, kwargs: dict) -> None:
491             for key, val in kwargs.items():
492                 if key in self._panel:
493                     self._panel[key] = val
494                 else:
495                     raise KeyError(f"The key {key} is not defined.")
496
497         @property
498         def defaults(self) -> dict:
499             return self._defaults
500
501         @property
502         def panel(self) -> dict:
503             return self._panel
504
505         def get(self, key: str) -> any:
506             return self._panel[key]
507
508         def values(self) -> list:
509             return list(self._panel.values())
510
511     @staticmethod
512     def _generate_options(opts: list) -> list:
513         """
514         """
515         return [{"label": i, "value": i} for i in opts]
516
517     def callbacks(self, app):
518
519         @app.callback(
520             Output("control-panel", "data"),  # Store
521             Output("graph-passed", "figure"),
522             Output("graph-duration", "figure"),
523             Output("ri-ttypes", "options"),
524             Output("ri-cadences", "options"),
525             Output("dd-tbeds", "options"),
526             Output("ri-duts", "value"),
527             Output("ri-ttypes", "value"),
528             Output("ri-cadences", "value"),
529             Output("dd-tbeds", "value"),
530             Output("al-job", "children"),
531             State("control-panel", "data"),  # Store
532             Input("ri-duts", "value"),
533             Input("ri-ttypes", "value"),
534             Input("ri-cadences", "value"),
535             Input("dd-tbeds", "value"),
536             Input("dpr-period", "start_date"),
537             Input("dpr-period", "end_date"),
538             prevent_initial_call=True
539         )
540         def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
541                 tbed: str, d_start: str, d_end: str) -> tuple:
542             """
543             """
544
545             ctrl_panel = self.ControlPanel(cp_data, self.default)
546
547             d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
548                 int(d_start[8:10]))
549             d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
550
551             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
552             if trigger_id == "ri-duts":
553                 ttype_opts = self._generate_options(self._get_ttypes(dut))
554                 ttype_val = ttype_opts[0]["value"]
555                 cad_opts = self._generate_options(
556                     self._get_cadences(dut, ttype_val))
557                 cad_val = cad_opts[0]["value"]
558                 tbed_opts = self._generate_options(
559                     self._get_test_beds(dut, ttype_val, cad_val))
560                 tbed_val = tbed_opts[0]["value"]
561                 ctrl_panel.set({
562                     "ri-duts-value": dut,
563                     "ri-ttypes-options": ttype_opts,
564                     "ri-ttypes-value": ttype_val,
565                     "ri-cadences-options": cad_opts,
566                     "ri-cadences-value": cad_val,
567                     "dd-tbeds-options": tbed_opts,
568                     "dd-tbeds-value": tbed_val
569                 })
570             elif trigger_id == "ri-ttypes":
571                 cad_opts = self._generate_options(
572                     self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
573                 cad_val = cad_opts[0]["value"]
574                 tbed_opts = self._generate_options(
575                     self._get_test_beds(ctrl_panel.get("ri-duts-value"),
576                     ttype, cad_val))
577                 tbed_val = tbed_opts[0]["value"]
578                 ctrl_panel.set({
579                     "ri-ttypes-value": ttype,
580                     "ri-cadences-options": cad_opts,
581                     "ri-cadences-value": cad_val,
582                     "dd-tbeds-options": tbed_opts,
583                     "dd-tbeds-value": tbed_val
584                 })
585             elif trigger_id == "ri-cadences":
586                 tbed_opts = self._generate_options(
587                     self._get_test_beds(ctrl_panel.get("ri-duts-value"),
588                     ctrl_panel.get("ri-ttypes-value"), cadence))
589                 tbed_val = tbed_opts[0]["value"]
590                 ctrl_panel.set({
591                     "ri-cadences-value": cadence,
592                     "dd-tbeds-options": tbed_opts,
593                     "dd-tbeds-value": tbed_val
594                 })
595             elif trigger_id == "dd-tbeds":
596                 ctrl_panel.set({
597                     "dd-tbeds-value": tbed
598                 })
599             elif trigger_id == "dpr-period":
600                 pass
601
602             job = self._get_job(
603                 ctrl_panel.get("ri-duts-value"),
604                 ctrl_panel.get("ri-ttypes-value"),
605                 ctrl_panel.get("ri-cadences-value"),
606                 ctrl_panel.get("dd-tbeds-value")
607             )
608             ctrl_panel.set({"al-job-children": job})
609             fig_passed, fig_duration = graph_statistics(
610                 self.data, job, self.layout, d_start, d_end)
611
612             ret_val = [ctrl_panel.panel, fig_passed, fig_duration]
613             ret_val.extend(ctrl_panel.values())
614             return ret_val
615
616         @app.callback(
617             Output("download-data", "data"),
618             Input("btn-download-data", "n_clicks"),
619             prevent_initial_call=True
620         )
621         def _download_data(n_clicks):
622             """
623             """
624             if not n_clicks:
625                 raise PreventUpdate
626
627             return dcc.send_data_frame(self.data.to_csv, "statistics.csv")
628
629         @app.callback(
630             Output("row-metadata", "children"),
631             Output("offcanvas-metadata", "is_open"),
632             Input("graph-passed", "clickData"),
633             Input("graph-duration", "clickData"),
634             prevent_initial_call=True
635         )
636         def _show_metadata_from_graphs(
637                 passed_data: dict, duration_data: dict) -> tuple:
638             """
639             """
640
641             if not (passed_data or duration_data):
642                 raise PreventUpdate
643
644             metadata = no_update
645             open_canvas = False
646             title = "Job Statistics"
647             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
648             if trigger_id == "graph-passed":
649                 graph_data = passed_data["points"][0].get("hovertext", "")
650             elif trigger_id == "graph-duration":
651                 graph_data = duration_data["points"][0].get("text", "")
652             if graph_data:
653                 metadata = [
654                     dbc.Card(
655                         class_name="gy-2 p-0",
656                         children=[
657                             dbc.CardHeader(children=[
658                                 dcc.Clipboard(
659                                     target_id="metadata",
660                                     title="Copy",
661                                     style={"display": "inline-block"}
662                                 ),
663                                 title
664                             ]),
665                             dbc.CardBody(
666                                 id="metadata",
667                                 class_name="p-0",
668                                 children=[dbc.ListGroup(
669                                     children=[
670                                         dbc.ListGroupItem(
671                                             [
672                                                 dbc.Badge(
673                                                     x.split(":")[0]
674                                                 ),
675                                                 x.split(": ")[1]
676                                             ]
677                                         ) for x in graph_data.split("<br>")
678                                     ],
679                                     flush=True),
680                                 ]
681                             )
682                         ]
683                     )
684                 ]
685                 open_canvas = True
686
687             return metadata, open_canvas