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.
19 import dash_bootstrap_components as dbc
21 from flask import Flask
24 from dash import callback_context, no_update
25 from dash import Input, Output, State
26 from dash.exceptions import PreventUpdate
27 from yaml import load, FullLoader, YAMLError
28 from datetime import datetime, timedelta
29 from copy import deepcopy
31 from ..data.data import Data
32 from .graphs import graph_statistics
39 DEFAULT_JOB = "csit-vpp-perf-mrr-daily-master-2n-icx"
41 def __init__(self, app: Flask, html_layout_file: str, spec_file: str,
42 graph_layout_file: str, data_spec_file: str, tooltip_file: str,
43 time_period: int=None) -> None:
49 self._html_layout_file = html_layout_file
50 self._spec_file = spec_file
51 self._graph_layout_file = graph_layout_file
52 self._data_spec_file = data_spec_file
53 self._tooltip_file = tooltip_file
54 self._time_period = time_period
57 data_stats, data_mrr, data_ndrpdr = Data(
58 data_spec_file=self._data_spec_file,
60 ).read_stats(days=self._time_period)
62 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
64 # Pre-process the data:
65 data_stats = data_stats[~data_stats.job.str.contains("-verify-")]
66 data_stats = data_stats[~data_stats.job.str.contains("-coverage-")]
67 data_stats = data_stats[~data_stats.job.str.contains("-iterative-")]
68 data_stats = data_stats[["job", "build", "start_time", "duration"]]
71 (datetime.utcnow() - data_stats["start_time"].min()).days
72 if self._time_period > data_time_period:
73 self._time_period = data_time_period
75 jobs = sorted(list(data_stats["job"].unique()))
84 lst_job = job.split("-")
85 job_info["job"].append(job)
86 job_info["dut"].append(lst_job[1])
87 job_info["ttype"].append(lst_job[3])
88 job_info["cadence"].append(lst_job[4])
89 job_info["tbed"].append("-".join(lst_job[-2:]))
90 self.df_job_info = pd.DataFrame.from_dict(job_info)
92 lst_job = self.DEFAULT_JOB.split("-")
94 "job": self.DEFAULT_JOB,
97 "cadence": lst_job[4],
98 "tbed": "-".join(lst_job[-2:]),
99 "duts": self._generate_options(self._get_duts()),
100 "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
101 "cadences": self._generate_options(self._get_cadences(
102 lst_job[1], lst_job[3])),
103 "tbeds": self._generate_options(self._get_test_beds(
104 lst_job[1], lst_job[3], lst_job[4]))
111 "dut_version": list(),
117 # TODO: Add list of failed tests for each build
118 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
119 builds = df_job["build"].unique()
121 df_build = df_job.loc[(df_job["build"] == build)]
122 tst_info["job"].append(job)
123 tst_info["build"].append(build)
124 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
125 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
126 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
128 passed = df_build.value_counts(subset='passed')[True]
132 failed = df_build.value_counts(subset='passed')[False]
135 tst_info["passed"].append(passed)
136 tst_info["failed"].append(failed)
138 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
141 self._html_layout = ""
142 self._graph_layout = None
143 self._tooltips = dict()
146 with open(self._html_layout_file, "r") as file_read:
147 self._html_layout = file_read.read()
148 except IOError as err:
150 f"Not possible to open the file {self._html_layout_file}\n{err}"
154 with open(self._graph_layout_file, "r") as file_read:
155 self._graph_layout = load(file_read, Loader=FullLoader)
156 except IOError as err:
158 f"Not possible to open the file {self._graph_layout_file}\n"
161 except YAMLError as err:
163 f"An error occurred while parsing the specification file "
164 f"{self._graph_layout_file}\n{err}"
168 with open(self._tooltip_file, "r") as file_read:
169 self._tooltips = load(file_read, Loader=FullLoader)
170 except IOError as err:
172 f"Not possible to open the file {self._tooltip_file}\n{err}"
174 except YAMLError as err:
176 f"An error occurred while parsing the specification file "
177 f"{self._tooltip_file}\n{err}"
181 self._default_fig_passed, self._default_fig_duration = graph_statistics(
182 self.data, self._default["job"], self.layout
186 if self._app is not None and hasattr(self, 'callbacks'):
187 self.callbacks(self._app)
190 def html_layout(self) -> dict:
191 return self._html_layout
194 def data(self) -> pd.DataFrame:
198 def layout(self) -> dict:
199 return self._graph_layout
202 def time_period(self) -> int:
203 return self._time_period
206 def default(self) -> any:
209 def _get_duts(self) -> list:
212 return sorted(list(self.df_job_info["dut"].unique()))
214 def _get_ttypes(self, dut: str) -> list:
217 return sorted(list(self.df_job_info.loc[(
218 self.df_job_info["dut"] == dut
219 )]["ttype"].unique()))
221 def _get_cadences(self, dut: str, ttype: str) -> list:
224 return sorted(list(self.df_job_info.loc[(
225 (self.df_job_info["dut"] == dut) &
226 (self.df_job_info["ttype"] == ttype)
227 )]["cadence"].unique()))
229 def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list:
232 return sorted(list(self.df_job_info.loc[(
233 (self.df_job_info["dut"] == dut) &
234 (self.df_job_info["ttype"] == ttype) &
235 (self.df_job_info["cadence"] == cadence)
236 )]["tbed"].unique()))
238 def _get_job(self, dut, ttype, cadence, testbed):
239 """Get the name of a job defined by dut, ttype, cadence, testbed.
241 Input information comes from control panel.
243 return self.df_job_info.loc[(
244 (self.df_job_info["dut"] == dut) &
245 (self.df_job_info["ttype"] == ttype) &
246 (self.df_job_info["cadence"] == cadence) &
247 (self.df_job_info["tbed"] == testbed)
250 def _show_tooltip(self, id: str, title: str) -> list:
261 class_name="border ms-1",
264 children=self._tooltips.get(id, str()),
270 def add_content(self):
290 id="offcanvas-metadata",
291 title="Detailed Information",
295 dbc.Row(id="row-metadata")
303 self._add_ctrl_col(),
304 self._add_plotting_col(),
322 def _add_navbar(self):
323 """Add nav element with navigation panel. It is placed on the top.
325 return dbc.NavbarSimple(
326 id="navbarsimple-main",
330 "Continuous Performance Statistics",
339 brand_external_link=True,
344 def _add_ctrl_col(self) -> dbc.Col:
345 """Add column with controls. It is placed on the left side.
350 self._add_ctrl_panel(),
354 def _add_plotting_col(self) -> dbc.Col:
355 """Add column with plots and tables. It is placed on the right side.
358 id="col-plotting-area",
360 dbc.Row( # Passed / failed tests
361 id="row-graph-passed",
362 class_name="g-0 p-2",
364 dcc.Loading(children=[
367 figure=self._default_fig_passed
373 id="row-graph-duration",
374 class_name="g-0 p-2",
376 dcc.Loading(children=[
379 figure=self._default_fig_duration
385 id="row-btn-download",
386 class_name="g-0 p-2",
388 dcc.Loading(children=[
390 id="btn-download-data",
391 children=self._show_tooltip(
392 "help-download", "Download"),
396 dcc.Download(id="download-data")
404 def _add_ctrl_panel(self) -> dbc.Row:
412 class_name="g-0 p-2",
419 children=self._show_tooltip(
420 "help-dut", "Device under Test")
426 value=self.default["dut"],
427 options=self.default["duts"]
437 children=self._show_tooltip(
438 "help-ttype", "Test Type"),
443 value=self.default["ttype"],
444 options=self.default["ttypes"]
453 children=self._show_tooltip(
454 "help-cadence", "Cadence"),
459 value=self.default["cadence"],
460 options=self.default["cadences"]
469 children=self._show_tooltip(
470 "help-tbed", "Test Bed"),
474 placeholder="Select a test bed...",
475 value=self.default["tbed"],
476 options=self.default["tbeds"]
486 children=self.default["job"]
491 class_name="g-0 p-2",
495 children=self._show_tooltip(
496 "help-time-period", "Time Period"),
500 className="d-flex justify-content-center",
502 datetime.utcnow() - timedelta(
503 days=self.time_period),
504 max_date_allowed=datetime.utcnow(),
505 initial_visible_month=datetime.utcnow(),
507 datetime.utcnow() - timedelta(
508 days=self.time_period),
509 end_date=datetime.utcnow(),
510 display_format="D MMM YY"
520 def __init__(self, panel: dict, default: dict) -> None:
522 "ri-ttypes-options": default["ttypes"],
523 "ri-cadences-options": default["cadences"],
524 "dd-tbeds-options": default["tbeds"],
525 "ri-duts-value": default["dut"],
526 "ri-ttypes-value": default["ttype"],
527 "ri-cadences-value": default["cadence"],
528 "dd-tbeds-value": default["tbed"],
529 "al-job-children": default["job"]
531 self._panel = deepcopy(self._defaults)
533 for key in self._defaults:
534 self._panel[key] = panel[key]
536 def set(self, kwargs: dict) -> None:
537 for key, val in kwargs.items():
538 if key in self._panel:
539 self._panel[key] = val
541 raise KeyError(f"The key {key} is not defined.")
544 def defaults(self) -> dict:
545 return self._defaults
548 def panel(self) -> dict:
551 def get(self, key: str) -> any:
552 return self._panel[key]
554 def values(self) -> list:
555 return list(self._panel.values())
558 def _generate_options(opts: list) -> list:
561 return [{"label": i, "value": i} for i in opts]
563 def callbacks(self, app):
566 Output("control-panel", "data"), # Store
567 Output("graph-passed", "figure"),
568 Output("graph-duration", "figure"),
569 Output("ri-ttypes", "options"),
570 Output("ri-cadences", "options"),
571 Output("dd-tbeds", "options"),
572 Output("ri-duts", "value"),
573 Output("ri-ttypes", "value"),
574 Output("ri-cadences", "value"),
575 Output("dd-tbeds", "value"),
576 Output("al-job", "children"),
577 State("control-panel", "data"), # Store
578 Input("ri-duts", "value"),
579 Input("ri-ttypes", "value"),
580 Input("ri-cadences", "value"),
581 Input("dd-tbeds", "value"),
582 Input("dpr-period", "start_date"),
583 Input("dpr-period", "end_date"),
584 prevent_initial_call=True
586 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
587 tbed: str, d_start: str, d_end: str) -> tuple:
591 ctrl_panel = self.ControlPanel(cp_data, self.default)
593 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
595 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
597 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
598 if trigger_id == "ri-duts":
599 ttype_opts = self._generate_options(self._get_ttypes(dut))
600 ttype_val = ttype_opts[0]["value"]
601 cad_opts = self._generate_options(
602 self._get_cadences(dut, ttype_val))
603 cad_val = cad_opts[0]["value"]
604 tbed_opts = self._generate_options(
605 self._get_test_beds(dut, ttype_val, cad_val))
606 tbed_val = tbed_opts[0]["value"]
608 "ri-duts-value": dut,
609 "ri-ttypes-options": ttype_opts,
610 "ri-ttypes-value": ttype_val,
611 "ri-cadences-options": cad_opts,
612 "ri-cadences-value": cad_val,
613 "dd-tbeds-options": tbed_opts,
614 "dd-tbeds-value": tbed_val
616 elif trigger_id == "ri-ttypes":
617 cad_opts = self._generate_options(
618 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
619 cad_val = cad_opts[0]["value"]
620 tbed_opts = self._generate_options(
621 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
623 tbed_val = tbed_opts[0]["value"]
625 "ri-ttypes-value": ttype,
626 "ri-cadences-options": cad_opts,
627 "ri-cadences-value": cad_val,
628 "dd-tbeds-options": tbed_opts,
629 "dd-tbeds-value": tbed_val
631 elif trigger_id == "ri-cadences":
632 tbed_opts = self._generate_options(
633 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
634 ctrl_panel.get("ri-ttypes-value"), cadence))
635 tbed_val = tbed_opts[0]["value"]
637 "ri-cadences-value": cadence,
638 "dd-tbeds-options": tbed_opts,
639 "dd-tbeds-value": tbed_val
641 elif trigger_id == "dd-tbeds":
643 "dd-tbeds-value": tbed
645 elif trigger_id == "dpr-period":
649 ctrl_panel.get("ri-duts-value"),
650 ctrl_panel.get("ri-ttypes-value"),
651 ctrl_panel.get("ri-cadences-value"),
652 ctrl_panel.get("dd-tbeds-value")
654 ctrl_panel.set({"al-job-children": job})
655 fig_passed, fig_duration = graph_statistics(
656 self.data, job, self.layout, d_start, d_end)
658 ret_val = [ctrl_panel.panel, fig_passed, fig_duration]
659 ret_val.extend(ctrl_panel.values())
663 Output("download-data", "data"),
664 Input("btn-download-data", "n_clicks"),
665 prevent_initial_call=True
667 def _download_data(n_clicks):
673 return dcc.send_data_frame(self.data.to_csv, "statistics.csv")
676 Output("row-metadata", "children"),
677 Output("offcanvas-metadata", "is_open"),
678 Input("graph-passed", "clickData"),
679 Input("graph-duration", "clickData"),
680 prevent_initial_call=True
682 def _show_metadata_from_graphs(
683 passed_data: dict, duration_data: dict) -> tuple:
687 if not (passed_data or duration_data):
692 title = "Job Statistics"
693 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
694 if trigger_id == "graph-passed":
695 graph_data = passed_data["points"][0].get("hovertext", "")
696 elif trigger_id == "graph-duration":
697 graph_data = duration_data["points"][0].get("text", "")
701 class_name="gy-2 p-0",
703 dbc.CardHeader(children=[
705 target_id="metadata",
707 style={"display": "inline-block"}
714 children=[dbc.ListGroup(
723 ) for x in graph_data.split("<br>")
733 return metadata, open_canvas