0c5c22e81bdd4520e3f0f792d5bc6d58bd85c483
[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
25 from dash.exceptions import PreventUpdate
26 from yaml import load, FullLoader, YAMLError
27 from datetime import datetime, timedelta
28
29 from ..data.data import Data
30 from .graphs import graph_statistics
31
32
33 class Layout:
34     """
35     """
36
37     def __init__(self, app: Flask, html_layout_file: str, spec_file: str,
38         graph_layout_file: str, data_spec_file: str,
39         time_period: int=None) -> None:
40         """
41         """
42
43         # Inputs
44         self._app = app
45         self._html_layout_file = html_layout_file
46         self._spec_file = spec_file
47         self._graph_layout_file = graph_layout_file
48         self._data_spec_file = data_spec_file
49         self._time_period = time_period
50
51         # Read the data:
52         data_stats, data_mrr, data_ndrpdr = Data(
53             data_spec_file=self._data_spec_file,
54             debug=True
55         ).read_stats(days=self._time_period)
56
57         df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
58
59         # Pre-process the data:
60         data_stats = data_stats[~data_stats.job.str.contains("-verify-")]
61         data_stats = data_stats[~data_stats.job.str.contains("-coverage-")]
62         data_stats = data_stats[~data_stats.job.str.contains("-iterative-")]
63         data_stats = data_stats[["job", "build", "start_time", "duration"]]
64
65         data_time_period = \
66             (datetime.utcnow() - data_stats["start_time"].min()).days
67         if self._time_period > data_time_period:
68             self._time_period = data_time_period
69
70         self._jobs = sorted(list(data_stats["job"].unique()))
71
72         tst_info = {
73             "job": list(),
74             "build": list(),
75             "dut_type": list(),
76             "dut_version": list(),
77             "hosts": list(),
78             "passed": list(),
79             "failed": list()
80         }
81         for job in self._jobs:
82             df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
83             builds = df_job["build"].unique()
84             for build in builds:
85                 df_build = df_job.loc[(df_job["build"] == build)]
86                 tst_info["job"].append(job)
87                 tst_info["build"].append(build)
88                 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
89                 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
90                 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
91                 try:
92                     passed = df_build.value_counts(subset='passed')[True]
93                 except KeyError:
94                     passed = 0
95                 try:
96                     failed = df_build.value_counts(subset='passed')[False]
97                 except KeyError:
98                     failed = 0
99                 tst_info["passed"].append(passed)
100                 tst_info["failed"].append(failed)
101
102         self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
103
104         # Read from files:
105         self._html_layout = ""
106         self._graph_layout = None
107
108         try:
109             with open(self._html_layout_file, "r") as file_read:
110                 self._html_layout = file_read.read()
111         except IOError as err:
112             raise RuntimeError(
113                 f"Not possible to open the file {self._html_layout_file}\n{err}"
114             )
115
116         try:
117             with open(self._graph_layout_file, "r") as file_read:
118                 self._graph_layout = load(file_read, Loader=FullLoader)
119         except IOError as err:
120             raise RuntimeError(
121                 f"Not possible to open the file {self._graph_layout_file}\n"
122                 f"{err}"
123             )
124         except YAMLError as err:
125             raise RuntimeError(
126                 f"An error occurred while parsing the specification file "
127                 f"{self._graph_layout_file}\n"
128                 f"{err}"
129             )
130
131         self._default_fig_passed, self._default_fig_duration = graph_statistics(
132             self.data, self.jobs[0], self.layout
133         )
134
135         # Callbacks:
136         if self._app is not None and hasattr(self, 'callbacks'):
137             self.callbacks(self._app)
138
139     @property
140     def html_layout(self) -> dict:
141         return self._html_layout
142
143     @property
144     def data(self) -> pd.DataFrame:
145         return self._data
146
147     @property
148     def layout(self) -> dict:
149         return self._graph_layout
150
151     @property
152     def jobs(self) -> list:
153         return self._jobs
154
155     @property
156     def time_period(self):
157         return self._time_period
158
159     def add_content(self):
160         """
161         """
162         if self.html_layout:
163             return html.Div(
164                 id="div-main",
165                 children=[
166                     dbc.Row(
167                         id="row-navbar",
168                         class_name="g-0",
169                         children=[
170                             self._add_navbar(),
171                         ]
172                     ),
173                     dcc.Loading(
174                         dbc.Offcanvas(
175                             class_name="w-25",
176                             id="offcanvas-metadata",
177                             title="Detailed Information",
178                             placement="end",
179                             is_open=False,
180                             children=[
181                                 dbc.Row(id="row-metadata")
182                             ]
183                         )
184                     ),
185                     dbc.Row(
186                         id="row-main",
187                         class_name="g-0",
188                         children=[
189                             self._add_ctrl_col(),
190                             self._add_plotting_col(),
191                         ]
192                     )
193                 ]
194             )
195         else:
196             return html.Div(
197                 id="div-main-error",
198                 children=[
199                     dbc.Alert(
200                         [
201                             "An Error Occured",
202                         ],
203                         color="danger",
204                     ),
205                 ]
206             )
207
208     def _add_navbar(self):
209         """Add nav element with navigation panel. It is placed on the top.
210         """
211         return dbc.NavbarSimple(
212             id="navbarsimple-main",
213             children=[
214                 dbc.NavItem(
215                     dbc.NavLink(
216                         "Continuous Performance Statistics",
217                         disabled=True,
218                         external_link=True,
219                         href="#"
220                     )
221                 )
222             ],
223             brand="Dashboard",
224             brand_href="/",
225             brand_external_link=True,
226             class_name="p-2",
227             fluid=True,
228         )
229
230     def _add_ctrl_col(self) -> dbc.Col:
231         """Add column with controls. It is placed on the left side.
232         """
233         return dbc.Col(
234             id="col-controls",
235             children=[
236                 self._add_ctrl_panel(),
237             ],
238         )
239
240     def _add_plotting_col(self) -> dbc.Col:
241         """Add column with plots and tables. It is placed on the right side.
242         """
243         return dbc.Col(
244             id="col-plotting-area",
245             children=[
246                 dbc.Row(  # Passed / failed tests
247                     id="row-graph-passed",
248                     class_name="g-0 p-2",
249                     children=[
250                         dcc.Loading(children=[
251                             dcc.Graph(
252                                 id="graph-passed",
253                                 figure=self._default_fig_passed
254                             )
255                         ])
256                     ]
257                 ),
258                 dbc.Row(  # Duration
259                     id="row-graph-duration",
260                     class_name="g-0 p-2",
261                     children=[
262                         dcc.Loading(children=[
263                             dcc.Graph(
264                                 id="graph-duration",
265                                 figure=self._default_fig_duration
266                             )
267                         ])
268                     ]
269                 ),
270                 dbc.Row(  # Download
271                     id="row-btn-download",
272                     class_name="g-0 p-2",
273                     children=[
274                         dcc.Loading(children=[
275                             dbc.Button(
276                                 id="btn-download-data",
277                                 children=["Download Data"],
278                                 class_name="me-1",
279                                 color="info"
280                             ),
281                             dcc.Download(id="download-data")
282                         ])
283                     ]
284                 )
285             ],
286             width=9,
287         )
288
289     def _add_ctrl_panel(self) -> dbc.Row:
290         """
291         """
292         return dbc.Row(
293             id="row-ctrl-panel",
294             class_name="g-0",
295             children=[
296                 dbc.Row(
297                     class_name="g-0 p-2",
298                     children=[
299                         dbc.Label("Choose the Trending Job"),
300                         dbc.RadioItems(
301                             id="ri_job",
302                             value=self.jobs[0],
303                             options=[
304                                 {"label": i, "value": i} for i in self.jobs
305                             ]
306                         )
307                     ]
308                 ),
309                 dbc.Row(
310                     class_name="g-0 p-2",
311                     children=[
312                         dbc.Label("Choose the Time Period"),
313                         dcc.DatePickerRange(
314                             id="dpr-period",
315                             className="d-flex justify-content-center",
316                             min_date_allowed=\
317                                 datetime.utcnow() - timedelta(
318                                     days=self.time_period),
319                             max_date_allowed=datetime.utcnow(),
320                             initial_visible_month=datetime.utcnow(),
321                             start_date=\
322                                 datetime.utcnow() - timedelta(
323                                     days=self.time_period),
324                             end_date=datetime.utcnow(),
325                             display_format="D MMMM YY"
326                         )
327                     ]
328                 )
329             ]
330         )
331
332     def callbacks(self, app):
333
334         @app.callback(
335             Output("graph-passed", "figure"),
336             Output("graph-duration", "figure"),
337             Input("ri_job", "value"),
338             Input("dpr-period", "start_date"),
339             Input("dpr-period", "end_date"),
340             prevent_initial_call=True
341         )
342         def _update_ctrl_panel(job:str, d_start: str, d_end: str) -> tuple:
343             """
344             """
345
346             d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
347                 int(d_start[8:10]))
348             d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
349
350             fig_passed, fig_duration = graph_statistics(
351                 self.data, job, self.layout, d_start, d_end
352             )
353
354             return fig_passed, fig_duration
355
356         @app.callback(
357             Output("download-data", "data"),
358             Input("btn-download-data", "n_clicks"),
359             prevent_initial_call=True
360         )
361         def _download_data(n_clicks):
362             """
363             """
364             if not n_clicks:
365                 raise PreventUpdate
366
367             return dcc.send_data_frame(self.data.to_csv, "statistics.csv")
368
369         @app.callback(
370             Output("row-metadata", "children"),
371             Output("offcanvas-metadata", "is_open"),
372             Input("graph-passed", "clickData"),
373             Input("graph-duration", "clickData"),
374             prevent_initial_call=True
375         )
376         def _show_metadata_from_graphs(
377                 passed_data: dict, duration_data: dict) -> tuple:
378             """
379             """
380
381             if not (passed_data or duration_data):
382                 raise PreventUpdate
383
384             metadata = no_update
385             open_canvas = False
386             title = "Job Statistics"
387             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
388             if trigger_id == "graph-passed":
389                 graph_data = passed_data["points"][0].get("hovertext", "")
390             elif trigger_id == "graph-duration":
391                 graph_data = duration_data["points"][0].get("text", "")
392             if graph_data:
393                 metadata = [
394                     dbc.Card(
395                         class_name="gy-2 p-0",
396                         children=[
397                             dbc.CardHeader(children=[
398                                 dcc.Clipboard(
399                                     target_id="metadata",
400                                     title="Copy",
401                                     style={"display": "inline-block"}
402                                 ),
403                                 title
404                             ]),
405                             dbc.CardBody(
406                                 id="metadata",
407                                 class_name="p-0",
408                                 children=[dbc.ListGroup(
409                                     children=[
410                                         dbc.ListGroupItem(
411                                             [
412                                                 dbc.Badge(
413                                                     x.split(":")[0]
414                                                 ),
415                                                 x.split(": ")[1]
416                                             ]
417                                         ) for x in graph_data.split("<br>")
418                                     ],
419                                     flush=True),
420                                 ]
421                             )
422                         ]
423                     )
424                 ]
425                 open_canvas = True
426
427             return metadata, open_canvas