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:
6 # http://www.apache.org/licenses/LICENSE-2.0
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.
14 """Plotly Dash HTML layout override.
18 import dash_bootstrap_components as dbc
22 from dash import Input, Output
23 from dash.exceptions import PreventUpdate
24 from yaml import load, FullLoader, YAMLError
25 from datetime import datetime, timedelta
27 from ..data.data import Data
28 from .graphs import graph_statistics
35 def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
42 self._html_layout_file = html_layout_file
43 self._spec_file = spec_file
44 self._graph_layout_file = graph_layout_file
45 self._data_spec_file = data_spec_file
48 data_stats, data_mrr, data_ndrpdr = Data(
49 data_spec_file=self._data_spec_file,
51 ).read_stats(days=180)
53 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
55 # Pre-process the data:
56 data_stats = data_stats[~data_stats.job.str.contains("-verify-")]
57 data_stats = data_stats[~data_stats.job.str.contains("-coverage-")]
58 data_stats = data_stats[~data_stats.job.str.contains("-iterative-")]
59 data_stats = data_stats[["job", "build", "start_time", "duration"]]
61 self._jobs = sorted(list(data_stats["job"].unique()))
67 "dut_version": list(),
72 for job in self._jobs:
73 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
74 builds = df_job["build"].unique()
76 df_build = df_job.loc[(df_job["build"] == build)]
77 tst_info["job"].append(job)
78 tst_info["build"].append(build)
79 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
80 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
81 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
83 passed = df_build.value_counts(subset='passed')[True]
87 failed = df_build.value_counts(subset='passed')[False]
90 tst_info["passed"].append(passed)
91 tst_info["failed"].append(failed)
93 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
96 self._html_layout = ""
97 self._graph_layout = None
100 with open(self._html_layout_file, "r") as file_read:
101 self._html_layout = file_read.read()
102 except IOError as err:
104 f"Not possible to open the file {self._html_layout_file}\n{err}"
108 with open(self._graph_layout_file, "r") as file_read:
109 self._graph_layout = load(file_read, Loader=FullLoader)
110 except IOError as err:
112 f"Not possible to open the file {self._graph_layout_file}\n"
115 except YAMLError as err:
117 f"An error occurred while parsing the specification file "
118 f"{self._graph_layout_file}\n"
122 self._default_fig_passed, self._default_fig_duration = graph_statistics(
123 self.data, self.jobs[0], self.layout
127 if self._app is not None and hasattr(self, 'callbacks'):
128 self.callbacks(self._app)
131 def html_layout(self) -> dict:
132 return self._html_layout
135 def data(self) -> pd.DataFrame:
139 def layout(self) -> dict:
140 return self._graph_layout
143 def jobs(self) -> list:
146 def add_content(self):
170 self._add_ctrl_col(),
171 self._add_plotting_col(),
189 def _add_navbar(self):
190 """Add nav element with navigation panel. It is placed on the top.
192 return dbc.NavbarSimple(
193 id="navbarsimple-main",
197 "Continuous Performance Statistics",
206 brand_external_link=True,
211 def _add_ctrl_col(self) -> dbc.Col:
212 """Add column with controls. It is placed on the left side.
217 self._add_ctrl_panel(),
221 def _add_plotting_col(self) -> dbc.Col:
222 """Add column with plots and tables. It is placed on the right side.
225 id="col-plotting-area",
227 dbc.Row( # Passed / failed tests
228 id="row-graph-passed",
229 class_name="g-0 p-2",
231 dcc.Loading(children=[
234 figure=self._default_fig_passed
240 id="row-graph-duration",
241 class_name="g-0 p-2",
243 dcc.Loading(children=[
246 figure=self._default_fig_duration
252 id="row-btn-download",
253 class_name="g-0 p-2",
255 dcc.Loading(children=[
257 id="btn-download-data",
258 children=["Download Data"]
260 dcc.Download(id="download-data")
268 def _add_ctrl_panel(self) -> dbc.Row:
273 class_name="g-0 p-2",
275 dbc.Label("Choose the Trending Job"),
279 options=[{"label": i, "value": i} for i in self.jobs]
281 dbc.Label("Choose the Time Period"),
284 className="d-flex justify-content-center",
286 datetime.utcnow()-timedelta(days=180),
287 max_date_allowed=datetime.utcnow(),
288 initial_visible_month=datetime.utcnow(),
289 start_date=datetime.utcnow() - timedelta(days=180),
290 end_date=datetime.utcnow(),
291 display_format="D MMMM YY"
296 def callbacks(self, app):
299 Output("graph-passed", "figure"),
300 Output("graph-duration", "figure"),
301 Input("ri_job", "value"),
302 Input("dpr-period", "start_date"),
303 Input("dpr-period", "end_date"),
304 prevent_initial_call=True
306 def _update_ctrl_panel(job:str, d_start: str, d_end: str) -> tuple:
310 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
312 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
314 fig_passed, fig_duration = graph_statistics(
315 self.data, job, self.layout, d_start, d_end
318 return fig_passed, fig_duration
321 Output("download-data", "data"),
322 Input("btn-download-data", "n_clicks"),
323 prevent_initial_call=True
325 def _download_data(n_clicks):
331 return dcc.send_data_frame(self.data.to_csv, "statistics.csv")