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(),
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]
127 failed_tests = df_build.loc[(df_build["passed"] == False)]\
128 ["test_id"].to_list()
130 for tst in failed_tests:
131 lst_tst = tst.split(".")
132 suite = lst_tst[-2].replace("2n1l-", "").\
133 replace("1n1l-", "").replace("2n-", "")
134 l_failed.append(f"{suite.split('-')[0]}-{lst_tst[-1]}")
138 tst_info["passed"].append(passed)
139 tst_info["failed"].append(failed)
140 tst_info["lst_failed"].append(sorted(l_failed))
142 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
145 self._html_layout = ""
146 self._graph_layout = None
147 self._tooltips = dict()
150 with open(self._html_layout_file, "r") as file_read:
151 self._html_layout = file_read.read()
152 except IOError as err:
154 f"Not possible to open the file {self._html_layout_file}\n{err}"
158 with open(self._graph_layout_file, "r") as file_read:
159 self._graph_layout = load(file_read, Loader=FullLoader)
160 except IOError as err:
162 f"Not possible to open the file {self._graph_layout_file}\n"
165 except YAMLError as err:
167 f"An error occurred while parsing the specification file "
168 f"{self._graph_layout_file}\n{err}"
172 with open(self._tooltip_file, "r") as file_read:
173 self._tooltips = load(file_read, Loader=FullLoader)
174 except IOError as err:
176 f"Not possible to open the file {self._tooltip_file}\n{err}"
178 except YAMLError as err:
180 f"An error occurred while parsing the specification file "
181 f"{self._tooltip_file}\n{err}"
185 self._default_fig_passed, self._default_fig_duration = graph_statistics(
186 self.data, self._default["job"], self.layout
190 if self._app is not None and hasattr(self, 'callbacks'):
191 self.callbacks(self._app)
194 def html_layout(self) -> dict:
195 return self._html_layout
198 def data(self) -> pd.DataFrame:
202 def layout(self) -> dict:
203 return self._graph_layout
206 def time_period(self) -> int:
207 return self._time_period
210 def default(self) -> any:
213 def _get_duts(self) -> list:
216 return sorted(list(self.df_job_info["dut"].unique()))
218 def _get_ttypes(self, dut: str) -> list:
221 return sorted(list(self.df_job_info.loc[(
222 self.df_job_info["dut"] == dut
223 )]["ttype"].unique()))
225 def _get_cadences(self, dut: str, ttype: str) -> list:
228 return sorted(list(self.df_job_info.loc[(
229 (self.df_job_info["dut"] == dut) &
230 (self.df_job_info["ttype"] == ttype)
231 )]["cadence"].unique()))
233 def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list:
236 return sorted(list(self.df_job_info.loc[(
237 (self.df_job_info["dut"] == dut) &
238 (self.df_job_info["ttype"] == ttype) &
239 (self.df_job_info["cadence"] == cadence)
240 )]["tbed"].unique()))
242 def _get_job(self, dut, ttype, cadence, testbed):
243 """Get the name of a job defined by dut, ttype, cadence, testbed.
245 Input information comes from control panel.
247 return self.df_job_info.loc[(
248 (self.df_job_info["dut"] == dut) &
249 (self.df_job_info["ttype"] == ttype) &
250 (self.df_job_info["cadence"] == cadence) &
251 (self.df_job_info["tbed"] == testbed)
254 def _set_job_params(self, job: str) -> dict:
257 lst_job = job.split("-")
262 "cadence": lst_job[4],
263 "tbed": "-".join(lst_job[-2:]),
264 "duts": self._generate_options(self._get_duts()),
265 "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
266 "cadences": self._generate_options(self._get_cadences(
267 lst_job[1], lst_job[3])),
268 "tbeds": self._generate_options(self._get_test_beds(
269 lst_job[1], lst_job[3], lst_job[4]))
272 def _show_tooltip(self, id: str, title: str,
273 clipboard_id: str=None) -> list:
277 dcc.Clipboard(target_id=clipboard_id, title="Copy URL") \
278 if clipboard_id else str(),
286 class_name="border ms-1",
289 children=self._tooltips.get(id, str()),
295 def add_content(self):
302 dcc.Store(id="control-panel"),
303 dcc.Location(id="url", refresh=False),
314 id="offcanvas-metadata",
315 title="Detailed Information",
319 dbc.Row(id="row-metadata")
327 self._add_ctrl_col(),
328 self._add_plotting_col(),
346 def _add_navbar(self):
347 """Add nav element with navigation panel. It is placed on the top.
349 return dbc.NavbarSimple(
350 id="navbarsimple-main",
354 "Continuous Performance Statistics",
363 brand_external_link=True,
368 def _add_ctrl_col(self) -> dbc.Col:
369 """Add column with controls. It is placed on the left side.
374 self._add_ctrl_panel(),
378 def _add_plotting_col(self) -> dbc.Col:
379 """Add column with plots and tables. It is placed on the right side.
382 id="col-plotting-area",
384 dbc.Row( # Passed / failed tests
385 id="row-graph-passed",
386 class_name="g-0 p-2",
388 dcc.Loading(children=[
391 figure=self._default_fig_passed
397 id="row-graph-duration",
398 class_name="g-0 p-2",
400 dcc.Loading(children=[
403 figure=self._default_fig_duration
409 class_name="g-0 p-2",
416 dcc.Loading(children=[
418 id="btn-download-data",
419 children=self._show_tooltip(
420 "help-download", "Download Data"),
424 dcc.Download(id="download-data")
435 style=self.URL_STYLE,
436 children=self._show_tooltip(
437 "help-url", "URL", "input-url")
443 style=self.URL_STYLE,
456 def _add_ctrl_panel(self) -> dbc.Row:
464 class_name="g-0 p-2",
471 children=self._show_tooltip(
472 "help-dut", "Device under Test")
478 value=self.default["dut"],
479 options=self.default["duts"]
489 children=self._show_tooltip(
490 "help-ttype", "Test Type"),
495 value=self.default["ttype"],
496 options=self.default["ttypes"]
505 children=self._show_tooltip(
506 "help-cadence", "Cadence"),
511 value=self.default["cadence"],
512 options=self.default["cadences"]
521 children=self._show_tooltip(
522 "help-tbed", "Test Bed"),
526 placeholder="Select a test bed...",
527 value=self.default["tbed"],
528 options=self.default["tbeds"]
538 children=self.default["job"]
543 class_name="g-0 p-2",
547 children=self._show_tooltip(
548 "help-time-period", "Time Period"),
552 className="d-flex justify-content-center",
554 datetime.utcnow() - timedelta(
555 days=self.time_period),
556 max_date_allowed=datetime.utcnow(),
557 initial_visible_month=datetime.utcnow(),
559 datetime.utcnow() - timedelta(
560 days=self.time_period),
561 end_date=datetime.utcnow(),
562 display_format="D MMM YY"
572 def __init__(self, panel: dict, default: dict) -> None:
574 "ri-ttypes-options": default["ttypes"],
575 "ri-cadences-options": default["cadences"],
576 "dd-tbeds-options": default["tbeds"],
577 "ri-duts-value": default["dut"],
578 "ri-ttypes-value": default["ttype"],
579 "ri-cadences-value": default["cadence"],
580 "dd-tbeds-value": default["tbed"],
581 "al-job-children": default["job"]
583 self._panel = deepcopy(self._defaults)
585 for key in self._defaults:
586 self._panel[key] = panel[key]
588 def set(self, kwargs: dict) -> None:
589 for key, val in kwargs.items():
590 if key in self._panel:
591 self._panel[key] = val
593 raise KeyError(f"The key {key} is not defined.")
596 def defaults(self) -> dict:
597 return self._defaults
600 def panel(self) -> dict:
603 def get(self, key: str) -> any:
604 return self._panel[key]
606 def values(self) -> list:
607 return list(self._panel.values())
610 def _generate_options(opts: list) -> list:
611 return [{"label": i, "value": i} for i in opts]
614 def _get_date(s_date: str) -> datetime:
615 return datetime(int(s_date[0:4]), int(s_date[5:7]), int(s_date[8:10]))
617 def callbacks(self, app):
620 Output("control-panel", "data"), # Store
621 Output("graph-passed", "figure"),
622 Output("graph-duration", "figure"),
623 Output("input-url", "value"),
624 Output("ri-ttypes", "options"),
625 Output("ri-cadences", "options"),
626 Output("dd-tbeds", "options"),
627 Output("ri-duts", "value"),
628 Output("ri-ttypes", "value"),
629 Output("ri-cadences", "value"),
630 Output("dd-tbeds", "value"),
631 Output("al-job", "children"),
632 State("control-panel", "data"), # Store
633 Input("ri-duts", "value"),
634 Input("ri-ttypes", "value"),
635 Input("ri-cadences", "value"),
636 Input("dd-tbeds", "value"),
637 Input("dpr-period", "start_date"),
638 Input("dpr-period", "end_date"),
640 # prevent_initial_call=True
642 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
643 tbed: str, start: str, end: str, href: str) -> tuple:
647 ctrl_panel = self.ControlPanel(cp_data, self.default)
649 start = self._get_date(start)
650 end = self._get_date(end)
653 parsed_url = url_decode(href)
655 url_params = parsed_url["params"]
659 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
660 if trigger_id == "ri-duts":
661 ttype_opts = self._generate_options(self._get_ttypes(dut))
662 ttype_val = ttype_opts[0]["value"]
663 cad_opts = self._generate_options(
664 self._get_cadences(dut, ttype_val))
665 cad_val = cad_opts[0]["value"]
666 tbed_opts = self._generate_options(
667 self._get_test_beds(dut, ttype_val, cad_val))
668 tbed_val = tbed_opts[0]["value"]
670 "ri-duts-value": dut,
671 "ri-ttypes-options": ttype_opts,
672 "ri-ttypes-value": ttype_val,
673 "ri-cadences-options": cad_opts,
674 "ri-cadences-value": cad_val,
675 "dd-tbeds-options": tbed_opts,
676 "dd-tbeds-value": tbed_val
678 elif trigger_id == "ri-ttypes":
679 cad_opts = self._generate_options(
680 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
681 cad_val = cad_opts[0]["value"]
682 tbed_opts = self._generate_options(
683 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
685 tbed_val = tbed_opts[0]["value"]
687 "ri-ttypes-value": ttype,
688 "ri-cadences-options": cad_opts,
689 "ri-cadences-value": cad_val,
690 "dd-tbeds-options": tbed_opts,
691 "dd-tbeds-value": tbed_val
693 elif trigger_id == "ri-cadences":
694 tbed_opts = self._generate_options(
695 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
696 ctrl_panel.get("ri-ttypes-value"), cadence))
697 tbed_val = tbed_opts[0]["value"]
699 "ri-cadences-value": cadence,
700 "dd-tbeds-options": tbed_opts,
701 "dd-tbeds-value": tbed_val
703 elif trigger_id == "dd-tbeds":
705 "dd-tbeds-value": tbed
707 elif trigger_id == "dpr-period":
709 elif trigger_id == "url":
710 # TODO: Add verification
712 new_job = url_params.get("job", list())[0]
713 new_start = url_params.get("start", list())[0]
714 new_end = url_params.get("end", list())[0]
715 if new_job and new_start and new_end:
716 start = self._get_date(new_start)
717 end = self._get_date(new_end)
718 job_params = self._set_job_params(new_job)
719 ctrl_panel = self.ControlPanel(None, job_params)
721 ctrl_panel = self.ControlPanel(cp_data, self.default)
723 ctrl_panel.get("ri-duts-value"),
724 ctrl_panel.get("ri-ttypes-value"),
725 ctrl_panel.get("ri-cadences-value"),
726 ctrl_panel.get("dd-tbeds-value")
730 ctrl_panel.get("ri-duts-value"),
731 ctrl_panel.get("ri-ttypes-value"),
732 ctrl_panel.get("ri-cadences-value"),
733 ctrl_panel.get("dd-tbeds-value")
736 ctrl_panel.set({"al-job-children": job})
737 fig_passed, fig_duration = graph_statistics(self.data, job,
738 self.layout, start, end)
741 new_url = url_encode({
742 "scheme": parsed_url["scheme"],
743 "netloc": parsed_url["netloc"],
744 "path": parsed_url["path"],
760 ret_val.extend(ctrl_panel.values())
764 Output("download-data", "data"),
765 State("control-panel", "data"), # Store
766 State("dpr-period", "start_date"),
767 State("dpr-period", "end_date"),
768 Input("btn-download-data", "n_clicks"),
769 prevent_initial_call=True
771 def _download_data(cp_data: dict, start: str, end: str, n_clicks: int):
777 ctrl_panel = self.ControlPanel(cp_data, self.default)
780 ctrl_panel.get("ri-duts-value"),
781 ctrl_panel.get("ri-ttypes-value"),
782 ctrl_panel.get("ri-cadences-value"),
783 ctrl_panel.get("dd-tbeds-value")
786 start = datetime(int(start[0:4]), int(start[5:7]), int(start[8:10]))
787 end = datetime(int(end[0:4]), int(end[5:7]), int(end[8:10]))
788 data = select_data(self.data, job, start, end)
789 data = data.drop(columns=["job", ])
791 return dcc.send_data_frame(data.T.to_csv, f"{job}-stats.csv")
794 Output("row-metadata", "children"),
795 Output("offcanvas-metadata", "is_open"),
796 Input("graph-passed", "clickData"),
797 Input("graph-duration", "clickData"),
798 prevent_initial_call=True
800 def _show_metadata_from_graphs(
801 passed_data: dict, duration_data: dict) -> tuple:
805 if not (passed_data or duration_data):
810 title = "Job Statistics"
811 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
812 if trigger_id == "graph-passed":
813 graph_data = passed_data["points"][0].get("hovertext", "")
814 elif trigger_id == "graph-duration":
815 graph_data = duration_data["points"][0].get("text", "")
817 lst_graph_data = graph_data.split("<br>")
819 # Prepare list of failed tests:
822 for itm in lst_graph_data:
823 if "csit-ref:" in itm:
824 job, build = itm.split(" ")[-1].split("/")
827 fail_tests = self.data.loc[
828 (self.data["job"] == job) &
829 (self.data["build"] == build)
830 ]["lst_failed"].values[0]
836 # Create the content of the offcanvas:
839 class_name="gy-2 p-0",
841 dbc.CardHeader(children=[
843 target_id="metadata",
845 style={"display": "inline-block"}
852 children=[dbc.ListGroup(
861 ) for x in lst_graph_data
870 if fail_tests is not None:
873 class_name="gy-2 p-0",
876 f"List of Failed Tests ({len(fail_tests)})"
881 children=[dbc.ListGroup(
883 dbc.ListGroupItem(x) \
895 return metadata, open_canvas