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 callback_context, no_update
23 from dash import Input, Output
24 from dash.exceptions import PreventUpdate
25 from yaml import load, FullLoader, YAMLError
26 from datetime import datetime, timedelta
28 from ..data.data import Data
29 from .graphs import graph_statistics
36 def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
43 self._html_layout_file = html_layout_file
44 self._spec_file = spec_file
45 self._graph_layout_file = graph_layout_file
46 self._data_spec_file = data_spec_file
49 data_stats, data_mrr, data_ndrpdr = Data(
50 data_spec_file=self._data_spec_file,
54 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
56 # Pre-process the data:
57 data_stats = data_stats[~data_stats.job.str.contains("-verify-")]
58 data_stats = data_stats[~data_stats.job.str.contains("-coverage-")]
59 data_stats = data_stats[~data_stats.job.str.contains("-iterative-")]
60 data_stats = data_stats[["job", "build", "start_time", "duration"]]
62 self._jobs = sorted(list(data_stats["job"].unique()))
68 "dut_version": list(),
73 for job in self._jobs:
74 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
75 builds = df_job["build"].unique()
77 df_build = df_job.loc[(df_job["build"] == build)]
78 tst_info["job"].append(job)
79 tst_info["build"].append(build)
80 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
81 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
82 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
84 passed = df_build.value_counts(subset='passed')[True]
88 failed = df_build.value_counts(subset='passed')[False]
91 tst_info["passed"].append(passed)
92 tst_info["failed"].append(failed)
94 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
97 self._html_layout = ""
98 self._graph_layout = None
101 with open(self._html_layout_file, "r") as file_read:
102 self._html_layout = file_read.read()
103 except IOError as err:
105 f"Not possible to open the file {self._html_layout_file}\n{err}"
109 with open(self._graph_layout_file, "r") as file_read:
110 self._graph_layout = load(file_read, Loader=FullLoader)
111 except IOError as err:
113 f"Not possible to open the file {self._graph_layout_file}\n"
116 except YAMLError as err:
118 f"An error occurred while parsing the specification file "
119 f"{self._graph_layout_file}\n"
123 self._default_fig_passed, self._default_fig_duration = graph_statistics(
124 self.data, self.jobs[0], self.layout
128 if self._app is not None and hasattr(self, 'callbacks'):
129 self.callbacks(self._app)
132 def html_layout(self) -> dict:
133 return self._html_layout
136 def data(self) -> pd.DataFrame:
140 def layout(self) -> dict:
141 return self._graph_layout
144 def jobs(self) -> list:
147 def add_content(self):
164 id="offcanvas-metadata",
165 title="Detailed Information",
169 dbc.Row(id="row-metadata")
177 self._add_ctrl_col(),
178 self._add_plotting_col(),
196 def _add_navbar(self):
197 """Add nav element with navigation panel. It is placed on the top.
199 return dbc.NavbarSimple(
200 id="navbarsimple-main",
204 "Continuous Performance Statistics",
213 brand_external_link=True,
218 def _add_ctrl_col(self) -> dbc.Col:
219 """Add column with controls. It is placed on the left side.
224 self._add_ctrl_panel(),
228 def _add_plotting_col(self) -> dbc.Col:
229 """Add column with plots and tables. It is placed on the right side.
232 id="col-plotting-area",
234 dbc.Row( # Passed / failed tests
235 id="row-graph-passed",
236 class_name="g-0 p-2",
238 dcc.Loading(children=[
241 figure=self._default_fig_passed
247 id="row-graph-duration",
248 class_name="g-0 p-2",
250 dcc.Loading(children=[
253 figure=self._default_fig_duration
259 id="row-btn-download",
260 class_name="g-0 p-2",
262 dcc.Loading(children=[
264 id="btn-download-data",
265 children=["Download Data"],
269 dcc.Download(id="download-data")
277 def _add_ctrl_panel(self) -> dbc.Row:
285 class_name="g-0 p-2",
287 dbc.Label("Choose the Trending Job"),
292 {"label": i, "value": i} for i in self.jobs
298 class_name="g-0 p-2",
300 dbc.Label("Choose the Time Period"),
303 className="d-flex justify-content-center",
305 datetime.utcnow()-timedelta(days=180),
306 max_date_allowed=datetime.utcnow(),
307 initial_visible_month=datetime.utcnow(),
308 start_date=datetime.utcnow() - timedelta(days=180),
309 end_date=datetime.utcnow(),
310 display_format="D MMMM YY"
317 def callbacks(self, app):
320 Output("graph-passed", "figure"),
321 Output("graph-duration", "figure"),
322 Input("ri_job", "value"),
323 Input("dpr-period", "start_date"),
324 Input("dpr-period", "end_date"),
325 prevent_initial_call=True
327 def _update_ctrl_panel(job:str, d_start: str, d_end: str) -> tuple:
331 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
333 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
335 fig_passed, fig_duration = graph_statistics(
336 self.data, job, self.layout, d_start, d_end
339 return fig_passed, fig_duration
342 Output("download-data", "data"),
343 Input("btn-download-data", "n_clicks"),
344 prevent_initial_call=True
346 def _download_data(n_clicks):
352 return dcc.send_data_frame(self.data.to_csv, "statistics.csv")
355 Output("row-metadata", "children"),
356 Output("offcanvas-metadata", "is_open"),
357 Input("graph-passed", "clickData"),
358 Input("graph-duration", "clickData"),
359 prevent_initial_call=True
361 def _show_metadata_from_graphs(
362 passed_data: dict, duration_data: dict) -> tuple:
366 if not (passed_data or duration_data):
371 title = "Job Statistics"
372 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
373 if trigger_id == "graph-passed":
374 graph_data = passed_data["points"][0].get("hovertext", "")
375 elif trigger_id == "graph-duration":
376 graph_data = duration_data["points"][0].get("text", "")
380 class_name="gy-2 p-0",
382 dbc.CardHeader(children=[
384 target_id="metadata",
386 style={"display": "inline-block"}
393 children=[dbc.ListGroup(
402 ) for x in graph_data.split("<br>")
412 return metadata, open_canvas