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
20 from flask import Flask
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
29 from ..data.data import Data
30 from .graphs import graph_statistics
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:
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
52 data_stats, data_mrr, data_ndrpdr = Data(
53 data_spec_file=self._data_spec_file,
55 ).read_stats(days=self._time_period)
57 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
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"]]
66 (datetime.utcnow() - data_stats["start_time"].min()).days
67 if self._time_period > data_time_period:
68 self._time_period = data_time_period
70 self._jobs = sorted(list(data_stats["job"].unique()))
76 "dut_version": list(),
81 for job in self._jobs:
82 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
83 builds = df_job["build"].unique()
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])
92 passed = df_build.value_counts(subset='passed')[True]
96 failed = df_build.value_counts(subset='passed')[False]
99 tst_info["passed"].append(passed)
100 tst_info["failed"].append(failed)
102 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
105 self._html_layout = ""
106 self._graph_layout = None
109 with open(self._html_layout_file, "r") as file_read:
110 self._html_layout = file_read.read()
111 except IOError as err:
113 f"Not possible to open the file {self._html_layout_file}\n{err}"
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:
121 f"Not possible to open the file {self._graph_layout_file}\n"
124 except YAMLError as err:
126 f"An error occurred while parsing the specification file "
127 f"{self._graph_layout_file}\n"
131 self._default_fig_passed, self._default_fig_duration = graph_statistics(
132 self.data, self.jobs[0], self.layout
136 if self._app is not None and hasattr(self, 'callbacks'):
137 self.callbacks(self._app)
140 def html_layout(self) -> dict:
141 return self._html_layout
144 def data(self) -> pd.DataFrame:
148 def layout(self) -> dict:
149 return self._graph_layout
152 def jobs(self) -> list:
156 def time_period(self):
157 return self._time_period
159 def add_content(self):
176 id="offcanvas-metadata",
177 title="Detailed Information",
181 dbc.Row(id="row-metadata")
189 self._add_ctrl_col(),
190 self._add_plotting_col(),
208 def _add_navbar(self):
209 """Add nav element with navigation panel. It is placed on the top.
211 return dbc.NavbarSimple(
212 id="navbarsimple-main",
216 "Continuous Performance Statistics",
225 brand_external_link=True,
230 def _add_ctrl_col(self) -> dbc.Col:
231 """Add column with controls. It is placed on the left side.
236 self._add_ctrl_panel(),
240 def _add_plotting_col(self) -> dbc.Col:
241 """Add column with plots and tables. It is placed on the right side.
244 id="col-plotting-area",
246 dbc.Row( # Passed / failed tests
247 id="row-graph-passed",
248 class_name="g-0 p-2",
250 dcc.Loading(children=[
253 figure=self._default_fig_passed
259 id="row-graph-duration",
260 class_name="g-0 p-2",
262 dcc.Loading(children=[
265 figure=self._default_fig_duration
271 id="row-btn-download",
272 class_name="g-0 p-2",
274 dcc.Loading(children=[
276 id="btn-download-data",
277 children=["Download Data"],
281 dcc.Download(id="download-data")
289 def _add_ctrl_panel(self) -> dbc.Row:
297 class_name="g-0 p-2",
299 dbc.Label("Choose the Trending Job"),
304 {"label": i, "value": i} for i in self.jobs
310 class_name="g-0 p-2",
312 dbc.Label("Choose the Time Period"),
315 className="d-flex justify-content-center",
317 datetime.utcnow() - timedelta(
318 days=self.time_period),
319 max_date_allowed=datetime.utcnow(),
320 initial_visible_month=datetime.utcnow(),
322 datetime.utcnow() - timedelta(
323 days=self.time_period),
324 end_date=datetime.utcnow(),
325 display_format="D MMMM YY"
332 def callbacks(self, app):
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
342 def _update_ctrl_panel(job:str, d_start: str, d_end: str) -> tuple:
346 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
348 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
350 fig_passed, fig_duration = graph_statistics(
351 self.data, job, self.layout, d_start, d_end
354 return fig_passed, fig_duration
357 Output("download-data", "data"),
358 Input("btn-download-data", "n_clicks"),
359 prevent_initial_call=True
361 def _download_data(n_clicks):
367 return dcc.send_data_frame(self.data.to_csv, "statistics.csv")
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
376 def _show_metadata_from_graphs(
377 passed_data: dict, duration_data: dict) -> tuple:
381 if not (passed_data or duration_data):
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", "")
395 class_name="gy-2 p-0",
397 dbc.CardHeader(children=[
399 target_id="metadata",
401 style={"display": "inline-block"}
408 children=[dbc.ListGroup(
417 ) for x in graph_data.split("<br>")
427 return metadata, open_canvas