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 ..utils.constants import Constants as C
32 from ..utils.url_processing import url_decode, url_encode
33 from ..data.data import Data
34 from .graphs import graph_statistics, select_data
41 def __init__(self, app: Flask, html_layout_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._graph_layout_file = graph_layout_file
51 self._data_spec_file = data_spec_file
52 self._tooltip_file = tooltip_file
53 self._time_period = time_period
56 data_stats, data_mrr, data_ndrpdr = Data(
57 data_spec_file=self._data_spec_file,
59 ).read_stats(days=self._time_period)
61 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
63 # Pre-process the data:
64 data_stats = data_stats[~data_stats.job.str.contains("-verify-")]
65 data_stats = data_stats[~data_stats.job.str.contains("-coverage-")]
66 data_stats = data_stats[~data_stats.job.str.contains("-iterative-")]
67 data_stats = data_stats[["job", "build", "start_time", "duration"]]
70 (datetime.utcnow() - data_stats["start_time"].min()).days
71 if self._time_period > data_time_period:
72 self._time_period = data_time_period
74 jobs = sorted(list(data_stats["job"].unique()))
83 lst_job = job.split("-")
84 job_info["job"].append(job)
85 job_info["dut"].append(lst_job[1])
86 job_info["ttype"].append(lst_job[3])
87 job_info["cadence"].append(lst_job[4])
88 job_info["tbed"].append("-".join(lst_job[-2:]))
89 self.df_job_info = pd.DataFrame.from_dict(job_info)
91 self._default = self._set_job_params(C.STATS_DEFAULT_JOB)
97 "dut_version": list(),
104 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
105 builds = df_job["build"].unique()
107 df_build = df_job.loc[(df_job["build"] == build)]
108 tst_info["job"].append(job)
109 tst_info["build"].append(build)
110 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
111 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
112 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
114 passed = df_build.value_counts(subset="passed")[True]
118 failed = df_build.value_counts(subset="passed")[False]
119 failed_tests = df_build.loc[(df_build["passed"] == False)]\
120 ["test_id"].to_list()
122 for tst in failed_tests:
123 lst_tst = tst.split(".")
124 suite = lst_tst[-2].replace("2n1l-", "").\
125 replace("1n1l-", "").replace("2n-", "")
126 l_failed.append(f"{suite.split('-')[0]}-{lst_tst[-1]}")
130 tst_info["passed"].append(passed)
131 tst_info["failed"].append(failed)
132 tst_info["lst_failed"].append(sorted(l_failed))
134 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
137 self._html_layout = ""
138 self._graph_layout = None
139 self._tooltips = dict()
142 with open(self._html_layout_file, "r") as file_read:
143 self._html_layout = file_read.read()
144 except IOError as err:
146 f"Not possible to open the file {self._html_layout_file}\n{err}"
150 with open(self._graph_layout_file, "r") as file_read:
151 self._graph_layout = load(file_read, Loader=FullLoader)
152 except IOError as err:
154 f"Not possible to open the file {self._graph_layout_file}\n"
157 except YAMLError as err:
159 f"An error occurred while parsing the specification file "
160 f"{self._graph_layout_file}\n{err}"
164 with open(self._tooltip_file, "r") as file_read:
165 self._tooltips = load(file_read, Loader=FullLoader)
166 except IOError as err:
168 f"Not possible to open the file {self._tooltip_file}\n{err}"
170 except YAMLError as err:
172 f"An error occurred while parsing the specification file "
173 f"{self._tooltip_file}\n{err}"
177 self._default_fig_passed, self._default_fig_duration = graph_statistics(
178 self.data, self._default["job"], self.layout
182 if self._app is not None and hasattr(self, 'callbacks'):
183 self.callbacks(self._app)
186 def html_layout(self) -> dict:
187 return self._html_layout
190 def data(self) -> pd.DataFrame:
194 def layout(self) -> dict:
195 return self._graph_layout
198 def time_period(self) -> int:
199 return self._time_period
202 def default(self) -> any:
205 def _get_duts(self) -> list:
208 return sorted(list(self.df_job_info["dut"].unique()))
210 def _get_ttypes(self, dut: str) -> list:
213 return sorted(list(self.df_job_info.loc[(
214 self.df_job_info["dut"] == dut
215 )]["ttype"].unique()))
217 def _get_cadences(self, dut: str, ttype: str) -> list:
220 return sorted(list(self.df_job_info.loc[(
221 (self.df_job_info["dut"] == dut) &
222 (self.df_job_info["ttype"] == ttype)
223 )]["cadence"].unique()))
225 def _get_test_beds(self, dut: str, ttype: str, cadence: 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 (self.df_job_info["cadence"] == cadence)
232 )]["tbed"].unique()))
234 def _get_job(self, dut, ttype, cadence, testbed):
235 """Get the name of a job defined by dut, ttype, cadence, testbed.
237 Input information comes from control panel.
239 return self.df_job_info.loc[(
240 (self.df_job_info["dut"] == dut) &
241 (self.df_job_info["ttype"] == ttype) &
242 (self.df_job_info["cadence"] == cadence) &
243 (self.df_job_info["tbed"] == testbed)
246 def _set_job_params(self, job: str) -> dict:
249 lst_job = job.split("-")
254 "cadence": lst_job[4],
255 "tbed": "-".join(lst_job[-2:]),
256 "duts": self._generate_options(self._get_duts()),
257 "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
258 "cadences": self._generate_options(self._get_cadences(
259 lst_job[1], lst_job[3])),
260 "tbeds": self._generate_options(self._get_test_beds(
261 lst_job[1], lst_job[3], lst_job[4]))
264 def _show_tooltip(self, id: str, title: str,
265 clipboard_id: str=None) -> list:
269 dcc.Clipboard(target_id=clipboard_id, title="Copy URL") \
270 if clipboard_id else str(),
278 class_name="border ms-1",
281 children=self._tooltips.get(id, str()),
287 def add_content(self):
294 dcc.Store(id="control-panel"),
295 dcc.Location(id="url", refresh=False),
306 id="offcanvas-metadata",
307 title="Detailed Information",
311 dbc.Row(id="row-metadata")
319 self._add_ctrl_col(),
320 self._add_plotting_col(),
338 def _add_navbar(self):
339 """Add nav element with navigation panel. It is placed on the top.
341 return dbc.NavbarSimple(
342 id="navbarsimple-main",
346 "Continuous Performance Statistics",
355 brand_external_link=True,
360 def _add_ctrl_col(self) -> dbc.Col:
361 """Add column with controls. It is placed on the left side.
366 self._add_ctrl_panel(),
370 def _add_plotting_col(self) -> dbc.Col:
371 """Add column with plots and tables. It is placed on the right side.
374 id="col-plotting-area",
376 dbc.Row( # Passed / failed tests
377 id="row-graph-passed",
378 class_name="g-0 p-2",
380 dcc.Loading(children=[
383 figure=self._default_fig_passed
389 id="row-graph-duration",
390 class_name="g-0 p-2",
392 dcc.Loading(children=[
395 figure=self._default_fig_duration
401 class_name="g-0 p-2",
408 dcc.Loading(children=[
410 id="btn-download-data",
411 children=self._show_tooltip(
412 "help-download", "Download Data"),
416 dcc.Download(id="download-data")
428 children=self._show_tooltip(
429 "help-url", "URL", "input-url")
448 def _add_ctrl_panel(self) -> dbc.Row:
456 class_name="g-0 p-2",
463 children=self._show_tooltip(
464 "help-dut", "Device under Test")
470 value=self.default["dut"],
471 options=self.default["duts"]
481 children=self._show_tooltip(
482 "help-ttype", "Test Type"),
487 value=self.default["ttype"],
488 options=self.default["ttypes"]
497 children=self._show_tooltip(
498 "help-cadence", "Cadence"),
503 value=self.default["cadence"],
504 options=self.default["cadences"]
513 children=self._show_tooltip(
514 "help-tbed", "Test Bed"),
518 placeholder="Select a test bed...",
519 value=self.default["tbed"],
520 options=self.default["tbeds"]
530 children=self.default["job"]
535 class_name="g-0 p-2",
539 children=self._show_tooltip(
540 "help-time-period", "Time Period"),
544 className="d-flex justify-content-center",
546 datetime.utcnow() - timedelta(
547 days=self.time_period),
548 max_date_allowed=datetime.utcnow(),
549 initial_visible_month=datetime.utcnow(),
551 datetime.utcnow() - timedelta(
552 days=self.time_period),
553 end_date=datetime.utcnow(),
554 display_format="D MMM YY"
564 def __init__(self, panel: dict, default: dict) -> None:
566 "ri-ttypes-options": default["ttypes"],
567 "ri-cadences-options": default["cadences"],
568 "dd-tbeds-options": default["tbeds"],
569 "ri-duts-value": default["dut"],
570 "ri-ttypes-value": default["ttype"],
571 "ri-cadences-value": default["cadence"],
572 "dd-tbeds-value": default["tbed"],
573 "al-job-children": default["job"]
575 self._panel = deepcopy(self._defaults)
577 for key in self._defaults:
578 self._panel[key] = panel[key]
580 def set(self, kwargs: dict) -> None:
581 for key, val in kwargs.items():
582 if key in self._panel:
583 self._panel[key] = val
585 raise KeyError(f"The key {key} is not defined.")
588 def defaults(self) -> dict:
589 return self._defaults
592 def panel(self) -> dict:
595 def get(self, key: str) -> any:
596 return self._panel[key]
598 def values(self) -> list:
599 return list(self._panel.values())
602 def _generate_options(opts: list) -> list:
603 return [{"label": i, "value": i} for i in opts]
606 def _get_date(s_date: str) -> datetime:
607 return datetime(int(s_date[0:4]), int(s_date[5:7]), int(s_date[8:10]))
609 def callbacks(self, app):
612 Output("control-panel", "data"), # Store
613 Output("graph-passed", "figure"),
614 Output("graph-duration", "figure"),
615 Output("input-url", "value"),
616 Output("ri-ttypes", "options"),
617 Output("ri-cadences", "options"),
618 Output("dd-tbeds", "options"),
619 Output("ri-duts", "value"),
620 Output("ri-ttypes", "value"),
621 Output("ri-cadences", "value"),
622 Output("dd-tbeds", "value"),
623 Output("al-job", "children"),
624 State("control-panel", "data"), # Store
625 Input("ri-duts", "value"),
626 Input("ri-ttypes", "value"),
627 Input("ri-cadences", "value"),
628 Input("dd-tbeds", "value"),
629 Input("dpr-period", "start_date"),
630 Input("dpr-period", "end_date"),
632 # prevent_initial_call=True
634 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
635 tbed: str, start: str, end: str, href: str) -> tuple:
639 ctrl_panel = self.ControlPanel(cp_data, self.default)
641 start = self._get_date(start)
642 end = self._get_date(end)
645 parsed_url = url_decode(href)
647 url_params = parsed_url["params"]
651 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
652 if trigger_id == "ri-duts":
653 ttype_opts = self._generate_options(self._get_ttypes(dut))
654 ttype_val = ttype_opts[0]["value"]
655 cad_opts = self._generate_options(
656 self._get_cadences(dut, ttype_val))
657 cad_val = cad_opts[0]["value"]
658 tbed_opts = self._generate_options(
659 self._get_test_beds(dut, ttype_val, cad_val))
660 tbed_val = tbed_opts[0]["value"]
662 "ri-duts-value": dut,
663 "ri-ttypes-options": ttype_opts,
664 "ri-ttypes-value": ttype_val,
665 "ri-cadences-options": cad_opts,
666 "ri-cadences-value": cad_val,
667 "dd-tbeds-options": tbed_opts,
668 "dd-tbeds-value": tbed_val
670 elif trigger_id == "ri-ttypes":
671 cad_opts = self._generate_options(
672 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
673 cad_val = cad_opts[0]["value"]
674 tbed_opts = self._generate_options(
675 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
677 tbed_val = tbed_opts[0]["value"]
679 "ri-ttypes-value": ttype,
680 "ri-cadences-options": cad_opts,
681 "ri-cadences-value": cad_val,
682 "dd-tbeds-options": tbed_opts,
683 "dd-tbeds-value": tbed_val
685 elif trigger_id == "ri-cadences":
686 tbed_opts = self._generate_options(
687 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
688 ctrl_panel.get("ri-ttypes-value"), cadence))
689 tbed_val = tbed_opts[0]["value"]
691 "ri-cadences-value": cadence,
692 "dd-tbeds-options": tbed_opts,
693 "dd-tbeds-value": tbed_val
695 elif trigger_id == "dd-tbeds":
697 "dd-tbeds-value": tbed
699 elif trigger_id == "dpr-period":
701 elif trigger_id == "url":
702 # TODO: Add verification
704 new_job = url_params.get("job", list())[0]
705 new_start = url_params.get("start", list())[0]
706 new_end = url_params.get("end", list())[0]
707 if new_job and new_start and new_end:
708 start = self._get_date(new_start)
709 end = self._get_date(new_end)
710 job_params = self._set_job_params(new_job)
711 ctrl_panel = self.ControlPanel(None, job_params)
713 ctrl_panel = self.ControlPanel(cp_data, self.default)
715 ctrl_panel.get("ri-duts-value"),
716 ctrl_panel.get("ri-ttypes-value"),
717 ctrl_panel.get("ri-cadences-value"),
718 ctrl_panel.get("dd-tbeds-value")
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")
728 ctrl_panel.set({"al-job-children": job})
729 fig_passed, fig_duration = graph_statistics(self.data, job,
730 self.layout, start, end)
733 new_url = url_encode({
734 "scheme": parsed_url["scheme"],
735 "netloc": parsed_url["netloc"],
736 "path": parsed_url["path"],
752 ret_val.extend(ctrl_panel.values())
756 Output("download-data", "data"),
757 State("control-panel", "data"), # Store
758 State("dpr-period", "start_date"),
759 State("dpr-period", "end_date"),
760 Input("btn-download-data", "n_clicks"),
761 prevent_initial_call=True
763 def _download_data(cp_data: dict, start: str, end: str, n_clicks: int):
769 ctrl_panel = self.ControlPanel(cp_data, self.default)
772 ctrl_panel.get("ri-duts-value"),
773 ctrl_panel.get("ri-ttypes-value"),
774 ctrl_panel.get("ri-cadences-value"),
775 ctrl_panel.get("dd-tbeds-value")
778 start = datetime(int(start[0:4]), int(start[5:7]), int(start[8:10]))
779 end = datetime(int(end[0:4]), int(end[5:7]), int(end[8:10]))
780 data = select_data(self.data, job, start, end)
781 data = data.drop(columns=["job", ])
783 return dcc.send_data_frame(data.T.to_csv, f"{job}-stats.csv")
786 Output("row-metadata", "children"),
787 Output("offcanvas-metadata", "is_open"),
788 Input("graph-passed", "clickData"),
789 Input("graph-duration", "clickData"),
790 prevent_initial_call=True
792 def _show_metadata_from_graphs(
793 passed_data: dict, duration_data: dict) -> tuple:
797 if not (passed_data or duration_data):
802 title = "Job Statistics"
803 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
804 if trigger_id == "graph-passed":
805 graph_data = passed_data["points"][0].get("hovertext", "")
806 elif trigger_id == "graph-duration":
807 graph_data = duration_data["points"][0].get("text", "")
809 lst_graph_data = graph_data.split("<br>")
811 # Prepare list of failed tests:
814 for itm in lst_graph_data:
815 if "csit-ref:" in itm:
816 job, build = itm.split(" ")[-1].split("/")
819 fail_tests = self.data.loc[
820 (self.data["job"] == job) &
821 (self.data["build"] == build)
822 ]["lst_failed"].values[0]
828 # Create the content of the offcanvas:
831 class_name="gy-2 p-0",
833 dbc.CardHeader(children=[
835 target_id="metadata",
837 style={"display": "inline-block"}
844 children=[dbc.ListGroup(
853 ) for x in lst_graph_data
862 if fail_tests is not None:
865 class_name="gy-2 p-0",
868 f"List of Failed Tests ({len(fail_tests)})"
873 children=[dbc.ListGroup(
875 dbc.ListGroupItem(x) \
887 return metadata, open_canvas