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,
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._graph_layout_file = graph_layout_file
58 self._data_spec_file = data_spec_file
59 self._tooltip_file = tooltip_file
60 self._time_period = time_period
63 data_stats, data_mrr, data_ndrpdr = Data(
64 data_spec_file=self._data_spec_file,
66 ).read_stats(days=self._time_period)
68 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
70 # Pre-process the data:
71 data_stats = data_stats[~data_stats.job.str.contains("-verify-")]
72 data_stats = data_stats[~data_stats.job.str.contains("-coverage-")]
73 data_stats = data_stats[~data_stats.job.str.contains("-iterative-")]
74 data_stats = data_stats[["job", "build", "start_time", "duration"]]
77 (datetime.utcnow() - data_stats["start_time"].min()).days
78 if self._time_period > data_time_period:
79 self._time_period = data_time_period
81 jobs = sorted(list(data_stats["job"].unique()))
90 lst_job = job.split("-")
91 job_info["job"].append(job)
92 job_info["dut"].append(lst_job[1])
93 job_info["ttype"].append(lst_job[3])
94 job_info["cadence"].append(lst_job[4])
95 job_info["tbed"].append("-".join(lst_job[-2:]))
96 self.df_job_info = pd.DataFrame.from_dict(job_info)
98 self._default = self._set_job_params(self.DEFAULT_JOB)
104 "dut_version": list(),
111 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
112 builds = df_job["build"].unique()
114 df_build = df_job.loc[(df_job["build"] == build)]
115 tst_info["job"].append(job)
116 tst_info["build"].append(build)
117 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
118 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
119 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
121 passed = df_build.value_counts(subset="passed")[True]
125 failed = df_build.value_counts(subset="passed")[False]
126 failed_tests = df_build.loc[(df_build["passed"] == False)]\
127 ["test_id"].to_list()
129 for tst in failed_tests:
130 lst_tst = tst.split(".")
131 suite = lst_tst[-2].replace("2n1l-", "").\
132 replace("1n1l-", "").replace("2n-", "")
133 l_failed.append(f"{suite.split('-')[0]}-{lst_tst[-1]}")
137 tst_info["passed"].append(passed)
138 tst_info["failed"].append(failed)
139 tst_info["lst_failed"].append(sorted(l_failed))
141 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
144 self._html_layout = ""
145 self._graph_layout = None
146 self._tooltips = dict()
149 with open(self._html_layout_file, "r") as file_read:
150 self._html_layout = file_read.read()
151 except IOError as err:
153 f"Not possible to open the file {self._html_layout_file}\n{err}"
157 with open(self._graph_layout_file, "r") as file_read:
158 self._graph_layout = load(file_read, Loader=FullLoader)
159 except IOError as err:
161 f"Not possible to open the file {self._graph_layout_file}\n"
164 except YAMLError as err:
166 f"An error occurred while parsing the specification file "
167 f"{self._graph_layout_file}\n{err}"
171 with open(self._tooltip_file, "r") as file_read:
172 self._tooltips = load(file_read, Loader=FullLoader)
173 except IOError as err:
175 f"Not possible to open the file {self._tooltip_file}\n{err}"
177 except YAMLError as err:
179 f"An error occurred while parsing the specification file "
180 f"{self._tooltip_file}\n{err}"
184 self._default_fig_passed, self._default_fig_duration = graph_statistics(
185 self.data, self._default["job"], self.layout
189 if self._app is not None and hasattr(self, 'callbacks'):
190 self.callbacks(self._app)
193 def html_layout(self) -> dict:
194 return self._html_layout
197 def data(self) -> pd.DataFrame:
201 def layout(self) -> dict:
202 return self._graph_layout
205 def time_period(self) -> int:
206 return self._time_period
209 def default(self) -> any:
212 def _get_duts(self) -> list:
215 return sorted(list(self.df_job_info["dut"].unique()))
217 def _get_ttypes(self, dut: str) -> list:
220 return sorted(list(self.df_job_info.loc[(
221 self.df_job_info["dut"] == dut
222 )]["ttype"].unique()))
224 def _get_cadences(self, dut: str, ttype: str) -> list:
227 return sorted(list(self.df_job_info.loc[(
228 (self.df_job_info["dut"] == dut) &
229 (self.df_job_info["ttype"] == ttype)
230 )]["cadence"].unique()))
232 def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list:
235 return sorted(list(self.df_job_info.loc[(
236 (self.df_job_info["dut"] == dut) &
237 (self.df_job_info["ttype"] == ttype) &
238 (self.df_job_info["cadence"] == cadence)
239 )]["tbed"].unique()))
241 def _get_job(self, dut, ttype, cadence, testbed):
242 """Get the name of a job defined by dut, ttype, cadence, testbed.
244 Input information comes from control panel.
246 return self.df_job_info.loc[(
247 (self.df_job_info["dut"] == dut) &
248 (self.df_job_info["ttype"] == ttype) &
249 (self.df_job_info["cadence"] == cadence) &
250 (self.df_job_info["tbed"] == testbed)
253 def _set_job_params(self, job: str) -> dict:
256 lst_job = job.split("-")
261 "cadence": lst_job[4],
262 "tbed": "-".join(lst_job[-2:]),
263 "duts": self._generate_options(self._get_duts()),
264 "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
265 "cadences": self._generate_options(self._get_cadences(
266 lst_job[1], lst_job[3])),
267 "tbeds": self._generate_options(self._get_test_beds(
268 lst_job[1], lst_job[3], lst_job[4]))
271 def _show_tooltip(self, id: str, title: str,
272 clipboard_id: str=None) -> list:
276 dcc.Clipboard(target_id=clipboard_id, title="Copy URL") \
277 if clipboard_id else str(),
285 class_name="border ms-1",
288 children=self._tooltips.get(id, str()),
294 def add_content(self):
301 dcc.Store(id="control-panel"),
302 dcc.Location(id="url", refresh=False),
313 id="offcanvas-metadata",
314 title="Detailed Information",
318 dbc.Row(id="row-metadata")
326 self._add_ctrl_col(),
327 self._add_plotting_col(),
345 def _add_navbar(self):
346 """Add nav element with navigation panel. It is placed on the top.
348 return dbc.NavbarSimple(
349 id="navbarsimple-main",
353 "Continuous Performance Statistics",
362 brand_external_link=True,
367 def _add_ctrl_col(self) -> dbc.Col:
368 """Add column with controls. It is placed on the left side.
373 self._add_ctrl_panel(),
377 def _add_plotting_col(self) -> dbc.Col:
378 """Add column with plots and tables. It is placed on the right side.
381 id="col-plotting-area",
383 dbc.Row( # Passed / failed tests
384 id="row-graph-passed",
385 class_name="g-0 p-2",
387 dcc.Loading(children=[
390 figure=self._default_fig_passed
396 id="row-graph-duration",
397 class_name="g-0 p-2",
399 dcc.Loading(children=[
402 figure=self._default_fig_duration
408 class_name="g-0 p-2",
415 dcc.Loading(children=[
417 id="btn-download-data",
418 children=self._show_tooltip(
419 "help-download", "Download Data"),
423 dcc.Download(id="download-data")
434 style=self.URL_STYLE,
435 children=self._show_tooltip(
436 "help-url", "URL", "input-url")
442 style=self.URL_STYLE,
455 def _add_ctrl_panel(self) -> dbc.Row:
463 class_name="g-0 p-2",
470 children=self._show_tooltip(
471 "help-dut", "Device under Test")
477 value=self.default["dut"],
478 options=self.default["duts"]
488 children=self._show_tooltip(
489 "help-ttype", "Test Type"),
494 value=self.default["ttype"],
495 options=self.default["ttypes"]
504 children=self._show_tooltip(
505 "help-cadence", "Cadence"),
510 value=self.default["cadence"],
511 options=self.default["cadences"]
520 children=self._show_tooltip(
521 "help-tbed", "Test Bed"),
525 placeholder="Select a test bed...",
526 value=self.default["tbed"],
527 options=self.default["tbeds"]
537 children=self.default["job"]
542 class_name="g-0 p-2",
546 children=self._show_tooltip(
547 "help-time-period", "Time Period"),
551 className="d-flex justify-content-center",
553 datetime.utcnow() - timedelta(
554 days=self.time_period),
555 max_date_allowed=datetime.utcnow(),
556 initial_visible_month=datetime.utcnow(),
558 datetime.utcnow() - timedelta(
559 days=self.time_period),
560 end_date=datetime.utcnow(),
561 display_format="D MMM YY"
571 def __init__(self, panel: dict, default: dict) -> None:
573 "ri-ttypes-options": default["ttypes"],
574 "ri-cadences-options": default["cadences"],
575 "dd-tbeds-options": default["tbeds"],
576 "ri-duts-value": default["dut"],
577 "ri-ttypes-value": default["ttype"],
578 "ri-cadences-value": default["cadence"],
579 "dd-tbeds-value": default["tbed"],
580 "al-job-children": default["job"]
582 self._panel = deepcopy(self._defaults)
584 for key in self._defaults:
585 self._panel[key] = panel[key]
587 def set(self, kwargs: dict) -> None:
588 for key, val in kwargs.items():
589 if key in self._panel:
590 self._panel[key] = val
592 raise KeyError(f"The key {key} is not defined.")
595 def defaults(self) -> dict:
596 return self._defaults
599 def panel(self) -> dict:
602 def get(self, key: str) -> any:
603 return self._panel[key]
605 def values(self) -> list:
606 return list(self._panel.values())
609 def _generate_options(opts: list) -> list:
610 return [{"label": i, "value": i} for i in opts]
613 def _get_date(s_date: str) -> datetime:
614 return datetime(int(s_date[0:4]), int(s_date[5:7]), int(s_date[8:10]))
616 def callbacks(self, app):
619 Output("control-panel", "data"), # Store
620 Output("graph-passed", "figure"),
621 Output("graph-duration", "figure"),
622 Output("input-url", "value"),
623 Output("ri-ttypes", "options"),
624 Output("ri-cadences", "options"),
625 Output("dd-tbeds", "options"),
626 Output("ri-duts", "value"),
627 Output("ri-ttypes", "value"),
628 Output("ri-cadences", "value"),
629 Output("dd-tbeds", "value"),
630 Output("al-job", "children"),
631 State("control-panel", "data"), # Store
632 Input("ri-duts", "value"),
633 Input("ri-ttypes", "value"),
634 Input("ri-cadences", "value"),
635 Input("dd-tbeds", "value"),
636 Input("dpr-period", "start_date"),
637 Input("dpr-period", "end_date"),
639 # prevent_initial_call=True
641 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
642 tbed: str, start: str, end: str, href: str) -> tuple:
646 ctrl_panel = self.ControlPanel(cp_data, self.default)
648 start = self._get_date(start)
649 end = self._get_date(end)
652 parsed_url = url_decode(href)
654 url_params = parsed_url["params"]
658 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
659 if trigger_id == "ri-duts":
660 ttype_opts = self._generate_options(self._get_ttypes(dut))
661 ttype_val = ttype_opts[0]["value"]
662 cad_opts = self._generate_options(
663 self._get_cadences(dut, ttype_val))
664 cad_val = cad_opts[0]["value"]
665 tbed_opts = self._generate_options(
666 self._get_test_beds(dut, ttype_val, cad_val))
667 tbed_val = tbed_opts[0]["value"]
669 "ri-duts-value": dut,
670 "ri-ttypes-options": ttype_opts,
671 "ri-ttypes-value": ttype_val,
672 "ri-cadences-options": cad_opts,
673 "ri-cadences-value": cad_val,
674 "dd-tbeds-options": tbed_opts,
675 "dd-tbeds-value": tbed_val
677 elif trigger_id == "ri-ttypes":
678 cad_opts = self._generate_options(
679 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
680 cad_val = cad_opts[0]["value"]
681 tbed_opts = self._generate_options(
682 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
684 tbed_val = tbed_opts[0]["value"]
686 "ri-ttypes-value": ttype,
687 "ri-cadences-options": cad_opts,
688 "ri-cadences-value": cad_val,
689 "dd-tbeds-options": tbed_opts,
690 "dd-tbeds-value": tbed_val
692 elif trigger_id == "ri-cadences":
693 tbed_opts = self._generate_options(
694 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
695 ctrl_panel.get("ri-ttypes-value"), cadence))
696 tbed_val = tbed_opts[0]["value"]
698 "ri-cadences-value": cadence,
699 "dd-tbeds-options": tbed_opts,
700 "dd-tbeds-value": tbed_val
702 elif trigger_id == "dd-tbeds":
704 "dd-tbeds-value": tbed
706 elif trigger_id == "dpr-period":
708 elif trigger_id == "url":
709 # TODO: Add verification
711 new_job = url_params.get("job", list())[0]
712 new_start = url_params.get("start", list())[0]
713 new_end = url_params.get("end", list())[0]
714 if new_job and new_start and new_end:
715 start = self._get_date(new_start)
716 end = self._get_date(new_end)
717 job_params = self._set_job_params(new_job)
718 ctrl_panel = self.ControlPanel(None, job_params)
720 ctrl_panel = self.ControlPanel(cp_data, self.default)
722 ctrl_panel.get("ri-duts-value"),
723 ctrl_panel.get("ri-ttypes-value"),
724 ctrl_panel.get("ri-cadences-value"),
725 ctrl_panel.get("dd-tbeds-value")
729 ctrl_panel.get("ri-duts-value"),
730 ctrl_panel.get("ri-ttypes-value"),
731 ctrl_panel.get("ri-cadences-value"),
732 ctrl_panel.get("dd-tbeds-value")
735 ctrl_panel.set({"al-job-children": job})
736 fig_passed, fig_duration = graph_statistics(self.data, job,
737 self.layout, start, end)
740 new_url = url_encode({
741 "scheme": parsed_url["scheme"],
742 "netloc": parsed_url["netloc"],
743 "path": parsed_url["path"],
759 ret_val.extend(ctrl_panel.values())
763 Output("download-data", "data"),
764 State("control-panel", "data"), # Store
765 State("dpr-period", "start_date"),
766 State("dpr-period", "end_date"),
767 Input("btn-download-data", "n_clicks"),
768 prevent_initial_call=True
770 def _download_data(cp_data: dict, start: str, end: str, n_clicks: int):
776 ctrl_panel = self.ControlPanel(cp_data, self.default)
779 ctrl_panel.get("ri-duts-value"),
780 ctrl_panel.get("ri-ttypes-value"),
781 ctrl_panel.get("ri-cadences-value"),
782 ctrl_panel.get("dd-tbeds-value")
785 start = datetime(int(start[0:4]), int(start[5:7]), int(start[8:10]))
786 end = datetime(int(end[0:4]), int(end[5:7]), int(end[8:10]))
787 data = select_data(self.data, job, start, end)
788 data = data.drop(columns=["job", ])
790 return dcc.send_data_frame(data.T.to_csv, f"{job}-stats.csv")
793 Output("row-metadata", "children"),
794 Output("offcanvas-metadata", "is_open"),
795 Input("graph-passed", "clickData"),
796 Input("graph-duration", "clickData"),
797 prevent_initial_call=True
799 def _show_metadata_from_graphs(
800 passed_data: dict, duration_data: dict) -> tuple:
804 if not (passed_data or duration_data):
809 title = "Job Statistics"
810 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
811 if trigger_id == "graph-passed":
812 graph_data = passed_data["points"][0].get("hovertext", "")
813 elif trigger_id == "graph-duration":
814 graph_data = duration_data["points"][0].get("text", "")
816 lst_graph_data = graph_data.split("<br>")
818 # Prepare list of failed tests:
821 for itm in lst_graph_data:
822 if "csit-ref:" in itm:
823 job, build = itm.split(" ")[-1].split("/")
826 fail_tests = self.data.loc[
827 (self.data["job"] == job) &
828 (self.data["build"] == build)
829 ]["lst_failed"].values[0]
835 # Create the content of the offcanvas:
838 class_name="gy-2 p-0",
840 dbc.CardHeader(children=[
842 target_id="metadata",
844 style={"display": "inline-block"}
851 children=[dbc.ListGroup(
860 ) for x in lst_graph_data
869 if fail_tests is not None:
872 class_name="gy-2 p-0",
875 f"List of Failed Tests ({len(fail_tests)})"
880 children=[dbc.ListGroup(
882 dbc.ListGroupItem(x) \
894 return metadata, open_canvas