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.utils import show_tooltip, gen_new_url
33 from ..utils.url_processing import url_decode
34 from ..data.data import Data
35 from .graphs import graph_statistics, select_data
42 def __init__(self, app: Flask, html_layout_file: str,
43 graph_layout_file: str, data_spec_file: str, tooltip_file: str,
44 time_period: int=None) -> None:
50 self._html_layout_file = html_layout_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 self._default = self._set_job_params(C.STATS_DEFAULT_JOB)
98 "dut_version": list(),
105 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
106 builds = df_job["build"].unique()
108 df_build = df_job.loc[(df_job["build"] == build)]
109 tst_info["job"].append(job)
110 tst_info["build"].append(build)
111 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
112 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
113 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
115 passed = df_build.value_counts(subset="passed")[True]
119 failed = df_build.value_counts(subset="passed")[False]
120 failed_tests = df_build.loc[(df_build["passed"] == False)]\
121 ["test_id"].to_list()
123 for tst in failed_tests:
124 lst_tst = tst.split(".")
125 suite = lst_tst[-2].replace("2n1l-", "").\
126 replace("1n1l-", "").replace("2n-", "")
127 l_failed.append(f"{suite.split('-')[0]}-{lst_tst[-1]}")
131 tst_info["passed"].append(passed)
132 tst_info["failed"].append(failed)
133 tst_info["lst_failed"].append(sorted(l_failed))
135 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
138 self._html_layout = ""
139 self._graph_layout = None
140 self._tooltips = dict()
143 with open(self._html_layout_file, "r") as file_read:
144 self._html_layout = file_read.read()
145 except IOError as err:
147 f"Not possible to open the file {self._html_layout_file}\n{err}"
151 with open(self._graph_layout_file, "r") as file_read:
152 self._graph_layout = load(file_read, Loader=FullLoader)
153 except IOError as err:
155 f"Not possible to open the file {self._graph_layout_file}\n"
158 except YAMLError as err:
160 f"An error occurred while parsing the specification file "
161 f"{self._graph_layout_file}\n{err}"
165 with open(self._tooltip_file, "r") as file_read:
166 self._tooltips = load(file_read, Loader=FullLoader)
167 except IOError as err:
169 f"Not possible to open the file {self._tooltip_file}\n{err}"
171 except YAMLError as err:
173 f"An error occurred while parsing the specification file "
174 f"{self._tooltip_file}\n{err}"
178 self._default_fig_passed, self._default_fig_duration = graph_statistics(
179 self.data, self._default["job"], self.layout
183 if self._app is not None and hasattr(self, 'callbacks'):
184 self.callbacks(self._app)
187 def html_layout(self) -> dict:
188 return self._html_layout
191 def data(self) -> pd.DataFrame:
195 def layout(self) -> dict:
196 return self._graph_layout
199 def time_period(self) -> int:
200 return self._time_period
203 def default(self) -> any:
206 def _get_duts(self) -> list:
209 return sorted(list(self.df_job_info["dut"].unique()))
211 def _get_ttypes(self, dut: str) -> list:
214 return sorted(list(self.df_job_info.loc[(
215 self.df_job_info["dut"] == dut
216 )]["ttype"].unique()))
218 def _get_cadences(self, dut: str, ttype: str) -> list:
221 return sorted(list(self.df_job_info.loc[(
222 (self.df_job_info["dut"] == dut) &
223 (self.df_job_info["ttype"] == ttype)
224 )]["cadence"].unique()))
226 def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list:
229 return sorted(list(self.df_job_info.loc[(
230 (self.df_job_info["dut"] == dut) &
231 (self.df_job_info["ttype"] == ttype) &
232 (self.df_job_info["cadence"] == cadence)
233 )]["tbed"].unique()))
235 def _get_job(self, dut, ttype, cadence, testbed):
236 """Get the name of a job defined by dut, ttype, cadence, testbed.
238 Input information comes from control panel.
240 return self.df_job_info.loc[(
241 (self.df_job_info["dut"] == dut) &
242 (self.df_job_info["ttype"] == ttype) &
243 (self.df_job_info["cadence"] == cadence) &
244 (self.df_job_info["tbed"] == testbed)
247 def _set_job_params(self, job: str) -> dict:
250 lst_job = job.split("-")
255 "cadence": lst_job[4],
256 "tbed": "-".join(lst_job[-2:]),
257 "duts": self._generate_options(self._get_duts()),
258 "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
259 "cadences": self._generate_options(self._get_cadences(
260 lst_job[1], lst_job[3])),
261 "tbeds": self._generate_options(self._get_test_beds(
262 lst_job[1], lst_job[3], lst_job[4]))
265 def add_content(self):
272 dcc.Store(id="control-panel"),
273 dcc.Location(id="url", refresh=False),
284 id="offcanvas-metadata",
285 title="Detailed Information",
289 dbc.Row(id="row-metadata")
297 self._add_ctrl_col(),
298 self._add_plotting_col(),
316 def _add_navbar(self):
317 """Add nav element with navigation panel. It is placed on the top.
319 return dbc.NavbarSimple(
320 id="navbarsimple-main",
324 "Continuous Performance Statistics",
333 brand_external_link=True,
338 def _add_ctrl_col(self) -> dbc.Col:
339 """Add column with controls. It is placed on the left side.
344 self._add_ctrl_panel(),
348 def _add_plotting_col(self) -> dbc.Col:
349 """Add column with plots and tables. It is placed on the right side.
352 id="col-plotting-area",
354 dbc.Row( # Passed / failed tests
355 id="row-graph-passed",
356 class_name="g-0 p-2",
358 dcc.Loading(children=[
361 figure=self._default_fig_passed
367 id="row-graph-duration",
368 class_name="g-0 p-2",
370 dcc.Loading(children=[
373 figure=self._default_fig_duration
379 class_name="g-0 p-2",
386 dcc.Loading(children=[
388 id="btn-download-data",
389 children=show_tooltip(self._tooltips,
390 "help-download", "Download Data"),
394 dcc.Download(id="download-data")
406 children=show_tooltip(
429 def _add_ctrl_panel(self) -> dbc.Row:
437 class_name="g-0 p-2",
444 children=show_tooltip(self._tooltips,
445 "help-dut", "Device under Test")
451 value=self.default["dut"],
452 options=self.default["duts"]
462 children=show_tooltip(self._tooltips,
463 "help-ttype", "Test Type"),
468 value=self.default["ttype"],
469 options=self.default["ttypes"]
478 children=show_tooltip(self._tooltips,
479 "help-cadence", "Cadence"),
484 value=self.default["cadence"],
485 options=self.default["cadences"]
494 children=show_tooltip(self._tooltips,
495 "help-tbed", "Test Bed"),
499 placeholder="Select a test bed...",
500 value=self.default["tbed"],
501 options=self.default["tbeds"]
511 children=self.default["job"]
516 class_name="g-0 p-2",
520 children=show_tooltip(self._tooltips,
521 "help-time-period", "Time Period"),
525 className="d-flex justify-content-center",
527 datetime.utcnow() - timedelta(
528 days=self.time_period),
529 max_date_allowed=datetime.utcnow(),
530 initial_visible_month=datetime.utcnow(),
532 datetime.utcnow() - timedelta(
533 days=self.time_period),
534 end_date=datetime.utcnow(),
535 display_format="D MMM YY"
545 def __init__(self, panel: dict, default: dict) -> None:
547 "ri-ttypes-options": default["ttypes"],
548 "ri-cadences-options": default["cadences"],
549 "dd-tbeds-options": default["tbeds"],
550 "ri-duts-value": default["dut"],
551 "ri-ttypes-value": default["ttype"],
552 "ri-cadences-value": default["cadence"],
553 "dd-tbeds-value": default["tbed"],
554 "al-job-children": default["job"]
556 self._panel = deepcopy(self._defaults)
558 for key in self._defaults:
559 self._panel[key] = panel[key]
561 def set(self, kwargs: dict) -> None:
562 for key, val in kwargs.items():
563 if key in self._panel:
564 self._panel[key] = val
566 raise KeyError(f"The key {key} is not defined.")
569 def defaults(self) -> dict:
570 return self._defaults
573 def panel(self) -> dict:
576 def get(self, key: str) -> any:
577 return self._panel[key]
579 def values(self) -> list:
580 return list(self._panel.values())
583 def _generate_options(opts: list) -> list:
584 return [{"label": i, "value": i} for i in opts]
587 def _get_date(s_date: str) -> datetime:
588 return datetime(int(s_date[0:4]), int(s_date[5:7]), int(s_date[8:10]))
590 def callbacks(self, app):
593 Output("control-panel", "data"), # Store
594 Output("graph-passed", "figure"),
595 Output("graph-duration", "figure"),
596 Output("input-url", "value"),
597 Output("ri-ttypes", "options"),
598 Output("ri-cadences", "options"),
599 Output("dd-tbeds", "options"),
600 Output("ri-duts", "value"),
601 Output("ri-ttypes", "value"),
602 Output("ri-cadences", "value"),
603 Output("dd-tbeds", "value"),
604 Output("al-job", "children"),
605 State("control-panel", "data"), # Store
606 Input("ri-duts", "value"),
607 Input("ri-ttypes", "value"),
608 Input("ri-cadences", "value"),
609 Input("dd-tbeds", "value"),
610 Input("dpr-period", "start_date"),
611 Input("dpr-period", "end_date"),
614 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
615 tbed: str, start: str, end: str, href: str) -> tuple:
619 ctrl_panel = self.ControlPanel(cp_data, self.default)
621 start = self._get_date(start)
622 end = self._get_date(end)
625 parsed_url = url_decode(href)
627 url_params = parsed_url["params"]
631 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
632 if trigger_id == "ri-duts":
633 ttype_opts = self._generate_options(self._get_ttypes(dut))
634 ttype_val = ttype_opts[0]["value"]
635 cad_opts = self._generate_options(
636 self._get_cadences(dut, ttype_val))
637 cad_val = cad_opts[0]["value"]
638 tbed_opts = self._generate_options(
639 self._get_test_beds(dut, ttype_val, cad_val))
640 tbed_val = tbed_opts[0]["value"]
642 "ri-duts-value": dut,
643 "ri-ttypes-options": ttype_opts,
644 "ri-ttypes-value": ttype_val,
645 "ri-cadences-options": cad_opts,
646 "ri-cadences-value": cad_val,
647 "dd-tbeds-options": tbed_opts,
648 "dd-tbeds-value": tbed_val
650 elif trigger_id == "ri-ttypes":
651 cad_opts = self._generate_options(
652 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
653 cad_val = cad_opts[0]["value"]
654 tbed_opts = self._generate_options(
655 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
657 tbed_val = tbed_opts[0]["value"]
659 "ri-ttypes-value": ttype,
660 "ri-cadences-options": cad_opts,
661 "ri-cadences-value": cad_val,
662 "dd-tbeds-options": tbed_opts,
663 "dd-tbeds-value": tbed_val
665 elif trigger_id == "ri-cadences":
666 tbed_opts = self._generate_options(
667 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
668 ctrl_panel.get("ri-ttypes-value"), cadence))
669 tbed_val = tbed_opts[0]["value"]
671 "ri-cadences-value": cadence,
672 "dd-tbeds-options": tbed_opts,
673 "dd-tbeds-value": tbed_val
675 elif trigger_id == "dd-tbeds":
677 "dd-tbeds-value": tbed
679 elif trigger_id == "dpr-period":
681 elif trigger_id == "url":
682 # TODO: Add verification
684 new_job = url_params.get("job", list())[0]
685 new_start = url_params.get("start", list())[0]
686 new_end = url_params.get("end", list())[0]
687 if new_job and new_start and new_end:
688 start = self._get_date(new_start)
689 end = self._get_date(new_end)
690 job_params = self._set_job_params(new_job)
691 ctrl_panel = self.ControlPanel(None, job_params)
693 ctrl_panel = self.ControlPanel(cp_data, self.default)
695 ctrl_panel.get("ri-duts-value"),
696 ctrl_panel.get("ri-ttypes-value"),
697 ctrl_panel.get("ri-cadences-value"),
698 ctrl_panel.get("dd-tbeds-value")
702 ctrl_panel.get("ri-duts-value"),
703 ctrl_panel.get("ri-ttypes-value"),
704 ctrl_panel.get("ri-cadences-value"),
705 ctrl_panel.get("dd-tbeds-value")
708 ctrl_panel.set({"al-job-children": job})
709 fig_passed, fig_duration = graph_statistics(self.data, job,
710 self.layout, start, end)
725 ret_val.extend(ctrl_panel.values())
729 Output("download-data", "data"),
730 State("control-panel", "data"), # Store
731 State("dpr-period", "start_date"),
732 State("dpr-period", "end_date"),
733 Input("btn-download-data", "n_clicks"),
734 prevent_initial_call=True
736 def _download_data(cp_data: dict, start: str, end: str, n_clicks: int):
742 ctrl_panel = self.ControlPanel(cp_data, self.default)
745 ctrl_panel.get("ri-duts-value"),
746 ctrl_panel.get("ri-ttypes-value"),
747 ctrl_panel.get("ri-cadences-value"),
748 ctrl_panel.get("dd-tbeds-value")
751 start = datetime(int(start[0:4]), int(start[5:7]), int(start[8:10]))
752 end = datetime(int(end[0:4]), int(end[5:7]), int(end[8:10]))
753 data = select_data(self.data, job, start, end)
754 data = data.drop(columns=["job", ])
756 return dcc.send_data_frame(data.T.to_csv, f"{job}-stats.csv")
759 Output("row-metadata", "children"),
760 Output("offcanvas-metadata", "is_open"),
761 Input("graph-passed", "clickData"),
762 Input("graph-duration", "clickData"),
763 prevent_initial_call=True
765 def _show_metadata_from_graphs(
766 passed_data: dict, duration_data: dict) -> tuple:
770 if not (passed_data or duration_data):
775 title = "Job Statistics"
776 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
777 if trigger_id == "graph-passed":
778 graph_data = passed_data["points"][0].get("hovertext", "")
779 elif trigger_id == "graph-duration":
780 graph_data = duration_data["points"][0].get("text", "")
782 lst_graph_data = graph_data.split("<br>")
784 # Prepare list of failed tests:
787 for itm in lst_graph_data:
788 if "csit-ref:" in itm:
789 job, build = itm.split(" ")[-1].split("/")
792 fail_tests = self.data.loc[
793 (self.data["job"] == job) &
794 (self.data["build"] == build)
795 ]["lst_failed"].values[0]
801 # Create the content of the offcanvas:
804 class_name="gy-2 p-0",
806 dbc.CardHeader(children=[
808 target_id="metadata",
810 style={"display": "inline-block"}
817 children=[dbc.ListGroup(
826 ) for x in lst_graph_data
835 if fail_tests is not None:
838 class_name="gy-2 p-0",
841 f"List of Failed Tests ({len(fail_tests)})"
846 children=[dbc.ListGroup(
848 dbc.ListGroupItem(x) \
860 return metadata, open_canvas