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, 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
30 from ..data.data import Data
31 from .graphs import graph_statistics
38 DEFAULT_JOB = "csit-vpp-perf-mrr-daily-master-2n-icx"
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:
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
55 data_stats, data_mrr, data_ndrpdr = Data(
56 data_spec_file=self._data_spec_file,
58 ).read_stats(days=self._time_period)
60 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
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"]]
69 (datetime.utcnow() - data_stats["start_time"].min()).days
70 if self._time_period > data_time_period:
71 self._time_period = data_time_period
73 jobs = sorted(list(data_stats["job"].unique()))
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)
90 lst_job = self.DEFAULT_JOB.split("-")
92 "job": self.DEFAULT_JOB,
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]))
109 "dut_version": list(),
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()
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])
126 passed = df_build.value_counts(subset='passed')[True]
130 failed = df_build.value_counts(subset='passed')[False]
133 tst_info["passed"].append(passed)
134 tst_info["failed"].append(failed)
136 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
139 self._html_layout = ""
140 self._graph_layout = None
143 with open(self._html_layout_file, "r") as file_read:
144 self._html_layout = file_read.read()
145 except IOError as err:
147 f"Not possible to open the file {self._html_layout_file}\n{err}"
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:
155 f"Not possible to open the file {self._graph_layout_file}\n"
158 except YAMLError as err:
160 f"An error occurred while parsing the specification file "
161 f"{self._graph_layout_file}\n"
165 self._default_fig_passed, self._default_fig_duration = graph_statistics(
166 self.data, self._default["job"], self.layout
170 if self._app is not None and hasattr(self, 'callbacks'):
171 self.callbacks(self._app)
174 def html_layout(self) -> dict:
175 return self._html_layout
178 def data(self) -> pd.DataFrame:
182 def layout(self) -> dict:
183 return self._graph_layout
186 def time_period(self) -> int:
187 return self._time_period
190 def default(self) -> any:
193 def _get_duts(self) -> list:
196 return sorted(list(self.df_job_info["dut"].unique()))
198 def _get_ttypes(self, dut: str) -> list:
201 return sorted(list(self.df_job_info.loc[(
202 self.df_job_info["dut"] == dut
203 )]["ttype"].unique()))
205 def _get_cadences(self, dut: str, ttype: str) -> list:
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()))
213 def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list:
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()))
222 def _get_job(self, dut, ttype, cadence, testbed):
223 """Get the name of a job defined by dut, ttype, cadence, testbed.
225 Input information comes from control panel.
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)
235 def add_content(self):
255 id="offcanvas-metadata",
256 title="Detailed Information",
260 dbc.Row(id="row-metadata")
268 self._add_ctrl_col(),
269 self._add_plotting_col(),
287 def _add_navbar(self):
288 """Add nav element with navigation panel. It is placed on the top.
290 return dbc.NavbarSimple(
291 id="navbarsimple-main",
295 "Continuous Performance Statistics",
304 brand_external_link=True,
309 def _add_ctrl_col(self) -> dbc.Col:
310 """Add column with controls. It is placed on the left side.
315 self._add_ctrl_panel(),
319 def _add_plotting_col(self) -> dbc.Col:
320 """Add column with plots and tables. It is placed on the right side.
323 id="col-plotting-area",
325 dbc.Row( # Passed / failed tests
326 id="row-graph-passed",
327 class_name="g-0 p-2",
329 dcc.Loading(children=[
332 figure=self._default_fig_passed
338 id="row-graph-duration",
339 class_name="g-0 p-2",
341 dcc.Loading(children=[
344 figure=self._default_fig_duration
350 id="row-btn-download",
351 class_name="g-0 p-2",
353 dcc.Loading(children=[
355 id="btn-download-data",
356 children=["Download Data"],
360 dcc.Download(id="download-data")
368 def _add_ctrl_panel(self) -> dbc.Row:
376 class_name="g-0 p-2",
388 value=self.default["dut"],
389 options=self.default["duts"]
403 value=self.default["ttype"],
404 options=self.default["ttypes"]
418 value=self.default["cadence"],
419 options=self.default["cadences"]
432 placeholder="Select a test bed...",
433 value=self.default["tbed"],
434 options=self.default["tbeds"]
444 children=self.default["job"]
451 class_name="g-0 p-2",
453 dbc.Label("Choose the Time Period"),
456 className="d-flex justify-content-center",
458 datetime.utcnow() - timedelta(
459 days=self.time_period),
460 max_date_allowed=datetime.utcnow(),
461 initial_visible_month=datetime.utcnow(),
463 datetime.utcnow() - timedelta(
464 days=self.time_period),
465 end_date=datetime.utcnow(),
466 display_format="D MMMM YY"
474 def __init__(self, panel: dict, default: dict) -> None:
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"]
485 self._panel = deepcopy(self._defaults)
487 for key in self._defaults:
488 self._panel[key] = panel[key]
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
495 raise KeyError(f"The key {key} is not defined.")
498 def defaults(self) -> dict:
499 return self._defaults
502 def panel(self) -> dict:
505 def get(self, key: str) -> any:
506 return self._panel[key]
508 def values(self) -> list:
509 return list(self._panel.values())
512 def _generate_options(opts: list) -> list:
515 return [{"label": i, "value": i} for i in opts]
517 def callbacks(self, app):
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
540 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
541 tbed: str, d_start: str, d_end: str) -> tuple:
545 ctrl_panel = self.ControlPanel(cp_data, self.default)
547 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
549 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
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"]
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
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"),
577 tbed_val = tbed_opts[0]["value"]
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
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"]
591 "ri-cadences-value": cadence,
592 "dd-tbeds-options": tbed_opts,
593 "dd-tbeds-value": tbed_val
595 elif trigger_id == "dd-tbeds":
597 "dd-tbeds-value": tbed
599 elif trigger_id == "dpr-period":
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")
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)
612 ret_val = [ctrl_panel.panel, fig_passed, fig_duration]
613 ret_val.extend(ctrl_panel.values())
617 Output("download-data", "data"),
618 Input("btn-download-data", "n_clicks"),
619 prevent_initial_call=True
621 def _download_data(n_clicks):
627 return dcc.send_data_frame(self.data.to_csv, "statistics.csv")
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
636 def _show_metadata_from_graphs(
637 passed_data: dict, duration_data: dict) -> tuple:
641 if not (passed_data or duration_data):
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", "")
655 class_name="gy-2 p-0",
657 dbc.CardHeader(children=[
659 target_id="metadata",
661 style={"display": "inline-block"}
668 children=[dbc.ListGroup(
677 ) for x in graph_data.split("<br>")
687 return metadata, open_canvas