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 ..data.url_processing import url_decode, url_encode
33 from .graphs import graph_statistics, select_data
40 DEFAULT_JOB = "csit-vpp-perf-mrr-daily-master-2n-icx"
43 "background-color": "#d2ebf5",
44 "border-color": "#bce1f1",
48 def __init__(self, app: Flask, html_layout_file: str, spec_file: str,
49 graph_layout_file: str, data_spec_file: str, tooltip_file: str,
50 time_period: int=None) -> None:
56 self._html_layout_file = html_layout_file
57 self._spec_file = spec_file
58 self._graph_layout_file = graph_layout_file
59 self._data_spec_file = data_spec_file
60 self._tooltip_file = tooltip_file
61 self._time_period = time_period
64 data_stats, data_mrr, data_ndrpdr = Data(
65 data_spec_file=self._data_spec_file,
67 ).read_stats(days=self._time_period)
69 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
71 # Pre-process the data:
72 data_stats = data_stats[~data_stats.job.str.contains("-verify-")]
73 data_stats = data_stats[~data_stats.job.str.contains("-coverage-")]
74 data_stats = data_stats[~data_stats.job.str.contains("-iterative-")]
75 data_stats = data_stats[["job", "build", "start_time", "duration"]]
78 (datetime.utcnow() - data_stats["start_time"].min()).days
79 if self._time_period > data_time_period:
80 self._time_period = data_time_period
82 jobs = sorted(list(data_stats["job"].unique()))
91 lst_job = job.split("-")
92 job_info["job"].append(job)
93 job_info["dut"].append(lst_job[1])
94 job_info["ttype"].append(lst_job[3])
95 job_info["cadence"].append(lst_job[4])
96 job_info["tbed"].append("-".join(lst_job[-2:]))
97 self.df_job_info = pd.DataFrame.from_dict(job_info)
99 self._default = self._set_job_params(self.DEFAULT_JOB)
105 "dut_version": list(),
111 # TODO: Add list of failed tests for each build
112 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
113 builds = df_job["build"].unique()
115 df_build = df_job.loc[(df_job["build"] == build)]
116 tst_info["job"].append(job)
117 tst_info["build"].append(build)
118 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
119 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
120 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
122 passed = df_build.value_counts(subset='passed')[True]
126 failed = df_build.value_counts(subset='passed')[False]
129 tst_info["passed"].append(passed)
130 tst_info["failed"].append(failed)
132 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
135 self._html_layout = ""
136 self._graph_layout = None
137 self._tooltips = dict()
140 with open(self._html_layout_file, "r") as file_read:
141 self._html_layout = file_read.read()
142 except IOError as err:
144 f"Not possible to open the file {self._html_layout_file}\n{err}"
148 with open(self._graph_layout_file, "r") as file_read:
149 self._graph_layout = load(file_read, Loader=FullLoader)
150 except IOError as err:
152 f"Not possible to open the file {self._graph_layout_file}\n"
155 except YAMLError as err:
157 f"An error occurred while parsing the specification file "
158 f"{self._graph_layout_file}\n{err}"
162 with open(self._tooltip_file, "r") as file_read:
163 self._tooltips = load(file_read, Loader=FullLoader)
164 except IOError as err:
166 f"Not possible to open the file {self._tooltip_file}\n{err}"
168 except YAMLError as err:
170 f"An error occurred while parsing the specification file "
171 f"{self._tooltip_file}\n{err}"
175 self._default_fig_passed, self._default_fig_duration = graph_statistics(
176 self.data, self._default["job"], self.layout
180 if self._app is not None and hasattr(self, 'callbacks'):
181 self.callbacks(self._app)
184 def html_layout(self) -> dict:
185 return self._html_layout
188 def data(self) -> pd.DataFrame:
192 def layout(self) -> dict:
193 return self._graph_layout
196 def time_period(self) -> int:
197 return self._time_period
200 def default(self) -> any:
203 def _get_duts(self) -> list:
206 return sorted(list(self.df_job_info["dut"].unique()))
208 def _get_ttypes(self, dut: str) -> list:
211 return sorted(list(self.df_job_info.loc[(
212 self.df_job_info["dut"] == dut
213 )]["ttype"].unique()))
215 def _get_cadences(self, dut: str, ttype: str) -> list:
218 return sorted(list(self.df_job_info.loc[(
219 (self.df_job_info["dut"] == dut) &
220 (self.df_job_info["ttype"] == ttype)
221 )]["cadence"].unique()))
223 def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list:
226 return sorted(list(self.df_job_info.loc[(
227 (self.df_job_info["dut"] == dut) &
228 (self.df_job_info["ttype"] == ttype) &
229 (self.df_job_info["cadence"] == cadence)
230 )]["tbed"].unique()))
232 def _get_job(self, dut, ttype, cadence, testbed):
233 """Get the name of a job defined by dut, ttype, cadence, testbed.
235 Input information comes from control panel.
237 return self.df_job_info.loc[(
238 (self.df_job_info["dut"] == dut) &
239 (self.df_job_info["ttype"] == ttype) &
240 (self.df_job_info["cadence"] == cadence) &
241 (self.df_job_info["tbed"] == testbed)
244 def _set_job_params(self, job: str) -> dict:
247 lst_job = job.split("-")
252 "cadence": lst_job[4],
253 "tbed": "-".join(lst_job[-2:]),
254 "duts": self._generate_options(self._get_duts()),
255 "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
256 "cadences": self._generate_options(self._get_cadences(
257 lst_job[1], lst_job[3])),
258 "tbeds": self._generate_options(self._get_test_beds(
259 lst_job[1], lst_job[3], lst_job[4]))
262 def _show_tooltip(self, id: str, title: str,
263 clipboard_id: str=None) -> list:
267 dcc.Clipboard(target_id=clipboard_id, title="Copy URL") \
268 if clipboard_id else str(),
276 class_name="border ms-1",
279 children=self._tooltips.get(id, str()),
285 def add_content(self):
292 dcc.Store(id="control-panel"),
293 dcc.Location(id="url", refresh=False),
304 id="offcanvas-metadata",
305 title="Detailed Information",
309 dbc.Row(id="row-metadata")
317 self._add_ctrl_col(),
318 self._add_plotting_col(),
336 def _add_navbar(self):
337 """Add nav element with navigation panel. It is placed on the top.
339 return dbc.NavbarSimple(
340 id="navbarsimple-main",
344 "Continuous Performance Statistics",
353 brand_external_link=True,
358 def _add_ctrl_col(self) -> dbc.Col:
359 """Add column with controls. It is placed on the left side.
364 self._add_ctrl_panel(),
368 def _add_plotting_col(self) -> dbc.Col:
369 """Add column with plots and tables. It is placed on the right side.
372 id="col-plotting-area",
374 dbc.Row( # Passed / failed tests
375 id="row-graph-passed",
376 class_name="g-0 p-2",
378 dcc.Loading(children=[
381 figure=self._default_fig_passed
387 id="row-graph-duration",
388 class_name="g-0 p-2",
390 dcc.Loading(children=[
393 figure=self._default_fig_duration
399 class_name="g-0 p-2",
406 dcc.Loading(children=[
408 id="btn-download-data",
409 children=self._show_tooltip(
410 "help-download", "Download Data"),
414 dcc.Download(id="download-data")
425 style=self.URL_STYLE,
426 children=self._show_tooltip(
427 "help-url", "URL", "input-url")
433 style=self.URL_STYLE,
446 def _add_ctrl_panel(self) -> dbc.Row:
454 class_name="g-0 p-2",
461 children=self._show_tooltip(
462 "help-dut", "Device under Test")
468 value=self.default["dut"],
469 options=self.default["duts"]
479 children=self._show_tooltip(
480 "help-ttype", "Test Type"),
485 value=self.default["ttype"],
486 options=self.default["ttypes"]
495 children=self._show_tooltip(
496 "help-cadence", "Cadence"),
501 value=self.default["cadence"],
502 options=self.default["cadences"]
511 children=self._show_tooltip(
512 "help-tbed", "Test Bed"),
516 placeholder="Select a test bed...",
517 value=self.default["tbed"],
518 options=self.default["tbeds"]
528 children=self.default["job"]
533 class_name="g-0 p-2",
537 children=self._show_tooltip(
538 "help-time-period", "Time Period"),
542 className="d-flex justify-content-center",
544 datetime.utcnow() - timedelta(
545 days=self.time_period),
546 max_date_allowed=datetime.utcnow(),
547 initial_visible_month=datetime.utcnow(),
549 datetime.utcnow() - timedelta(
550 days=self.time_period),
551 end_date=datetime.utcnow(),
552 display_format="D MMM YY"
562 def __init__(self, panel: dict, default: dict) -> None:
564 "ri-ttypes-options": default["ttypes"],
565 "ri-cadences-options": default["cadences"],
566 "dd-tbeds-options": default["tbeds"],
567 "ri-duts-value": default["dut"],
568 "ri-ttypes-value": default["ttype"],
569 "ri-cadences-value": default["cadence"],
570 "dd-tbeds-value": default["tbed"],
571 "al-job-children": default["job"]
573 self._panel = deepcopy(self._defaults)
575 for key in self._defaults:
576 self._panel[key] = panel[key]
578 def set(self, kwargs: dict) -> None:
579 for key, val in kwargs.items():
580 if key in self._panel:
581 self._panel[key] = val
583 raise KeyError(f"The key {key} is not defined.")
586 def defaults(self) -> dict:
587 return self._defaults
590 def panel(self) -> dict:
593 def get(self, key: str) -> any:
594 return self._panel[key]
596 def values(self) -> list:
597 return list(self._panel.values())
600 def _generate_options(opts: list) -> list:
601 return [{"label": i, "value": i} for i in opts]
604 def _get_date(s_date: str) -> datetime:
605 return datetime(int(s_date[0:4]), int(s_date[5:7]), int(s_date[8:10]))
607 def callbacks(self, app):
610 Output("control-panel", "data"), # Store
611 Output("graph-passed", "figure"),
612 Output("graph-duration", "figure"),
613 Output("input-url", "value"),
614 Output("ri-ttypes", "options"),
615 Output("ri-cadences", "options"),
616 Output("dd-tbeds", "options"),
617 Output("ri-duts", "value"),
618 Output("ri-ttypes", "value"),
619 Output("ri-cadences", "value"),
620 Output("dd-tbeds", "value"),
621 Output("al-job", "children"),
622 State("control-panel", "data"), # Store
623 Input("ri-duts", "value"),
624 Input("ri-ttypes", "value"),
625 Input("ri-cadences", "value"),
626 Input("dd-tbeds", "value"),
627 Input("dpr-period", "start_date"),
628 Input("dpr-period", "end_date"),
630 # prevent_initial_call=True
632 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
633 tbed: str, start: str, end: str, href: str) -> tuple:
637 ctrl_panel = self.ControlPanel(cp_data, self.default)
639 start = self._get_date(start)
640 end = self._get_date(end)
643 parsed_url = url_decode(href)
645 url_params = parsed_url["params"]
649 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
650 if trigger_id == "ri-duts":
651 ttype_opts = self._generate_options(self._get_ttypes(dut))
652 ttype_val = ttype_opts[0]["value"]
653 cad_opts = self._generate_options(
654 self._get_cadences(dut, ttype_val))
655 cad_val = cad_opts[0]["value"]
656 tbed_opts = self._generate_options(
657 self._get_test_beds(dut, ttype_val, cad_val))
658 tbed_val = tbed_opts[0]["value"]
660 "ri-duts-value": dut,
661 "ri-ttypes-options": ttype_opts,
662 "ri-ttypes-value": ttype_val,
663 "ri-cadences-options": cad_opts,
664 "ri-cadences-value": cad_val,
665 "dd-tbeds-options": tbed_opts,
666 "dd-tbeds-value": tbed_val
668 elif trigger_id == "ri-ttypes":
669 cad_opts = self._generate_options(
670 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
671 cad_val = cad_opts[0]["value"]
672 tbed_opts = self._generate_options(
673 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
675 tbed_val = tbed_opts[0]["value"]
677 "ri-ttypes-value": ttype,
678 "ri-cadences-options": cad_opts,
679 "ri-cadences-value": cad_val,
680 "dd-tbeds-options": tbed_opts,
681 "dd-tbeds-value": tbed_val
683 elif trigger_id == "ri-cadences":
684 tbed_opts = self._generate_options(
685 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
686 ctrl_panel.get("ri-ttypes-value"), cadence))
687 tbed_val = tbed_opts[0]["value"]
689 "ri-cadences-value": cadence,
690 "dd-tbeds-options": tbed_opts,
691 "dd-tbeds-value": tbed_val
693 elif trigger_id == "dd-tbeds":
695 "dd-tbeds-value": tbed
697 elif trigger_id == "dpr-period":
699 elif trigger_id == "url":
700 # TODO: Add verification
702 new_job = url_params.get("job", list())[0]
703 new_start = url_params.get("start", list())[0]
704 new_end = url_params.get("end", list())[0]
705 if new_job and new_start and new_end:
706 start = self._get_date(new_start)
707 end = self._get_date(new_end)
708 job_params = self._set_job_params(new_job)
709 ctrl_panel = self.ControlPanel(None, job_params)
711 ctrl_panel = self.ControlPanel(cp_data, self.default)
713 ctrl_panel.get("ri-duts-value"),
714 ctrl_panel.get("ri-ttypes-value"),
715 ctrl_panel.get("ri-cadences-value"),
716 ctrl_panel.get("dd-tbeds-value")
720 ctrl_panel.get("ri-duts-value"),
721 ctrl_panel.get("ri-ttypes-value"),
722 ctrl_panel.get("ri-cadences-value"),
723 ctrl_panel.get("dd-tbeds-value")
726 ctrl_panel.set({"al-job-children": job})
727 fig_passed, fig_duration = graph_statistics(self.data, job,
728 self.layout, start, end)
731 new_url = url_encode({
732 "scheme": parsed_url["scheme"],
733 "netloc": parsed_url["netloc"],
734 "path": parsed_url["path"],
750 ret_val.extend(ctrl_panel.values())
754 Output("download-data", "data"),
755 State("control-panel", "data"), # Store
756 State("dpr-period", "start_date"),
757 State("dpr-period", "end_date"),
758 Input("btn-download-data", "n_clicks"),
759 prevent_initial_call=True
761 def _download_data(cp_data: dict, start: str, end: str, n_clicks: int):
767 ctrl_panel = self.ControlPanel(cp_data, self.default)
770 ctrl_panel.get("ri-duts-value"),
771 ctrl_panel.get("ri-ttypes-value"),
772 ctrl_panel.get("ri-cadences-value"),
773 ctrl_panel.get("dd-tbeds-value")
776 start = datetime(int(start[0:4]), int(start[5:7]), int(start[8:10]))
777 end = datetime(int(end[0:4]), int(end[5:7]), int(end[8:10]))
778 data = select_data(self.data, job, start, end)
779 data = data.drop(columns=["job", ])
781 return dcc.send_data_frame(data.T.to_csv, f"{job}-stats.csv")
784 Output("row-metadata", "children"),
785 Output("offcanvas-metadata", "is_open"),
786 Input("graph-passed", "clickData"),
787 Input("graph-duration", "clickData"),
788 prevent_initial_call=True
790 def _show_metadata_from_graphs(
791 passed_data: dict, duration_data: dict) -> tuple:
795 if not (passed_data or duration_data):
800 title = "Job Statistics"
801 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
802 if trigger_id == "graph-passed":
803 graph_data = passed_data["points"][0].get("hovertext", "")
804 elif trigger_id == "graph-duration":
805 graph_data = duration_data["points"][0].get("text", "")
809 class_name="gy-2 p-0",
811 dbc.CardHeader(children=[
813 target_id="metadata",
815 style={"display": "inline-block"}
822 children=[dbc.ListGroup(
831 ) for x in graph_data.split("<br>")
841 return metadata, open_canvas