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"),
613 # prevent_initial_call=True
615 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
616 tbed: str, start: str, end: str, href: str) -> tuple:
620 ctrl_panel = self.ControlPanel(cp_data, self.default)
622 start = self._get_date(start)
623 end = self._get_date(end)
626 parsed_url = url_decode(href)
628 url_params = parsed_url["params"]
632 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
633 if trigger_id == "ri-duts":
634 ttype_opts = self._generate_options(self._get_ttypes(dut))
635 ttype_val = ttype_opts[0]["value"]
636 cad_opts = self._generate_options(
637 self._get_cadences(dut, ttype_val))
638 cad_val = cad_opts[0]["value"]
639 tbed_opts = self._generate_options(
640 self._get_test_beds(dut, ttype_val, cad_val))
641 tbed_val = tbed_opts[0]["value"]
643 "ri-duts-value": dut,
644 "ri-ttypes-options": ttype_opts,
645 "ri-ttypes-value": ttype_val,
646 "ri-cadences-options": cad_opts,
647 "ri-cadences-value": cad_val,
648 "dd-tbeds-options": tbed_opts,
649 "dd-tbeds-value": tbed_val
651 elif trigger_id == "ri-ttypes":
652 cad_opts = self._generate_options(
653 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
654 cad_val = cad_opts[0]["value"]
655 tbed_opts = self._generate_options(
656 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
658 tbed_val = tbed_opts[0]["value"]
660 "ri-ttypes-value": ttype,
661 "ri-cadences-options": cad_opts,
662 "ri-cadences-value": cad_val,
663 "dd-tbeds-options": tbed_opts,
664 "dd-tbeds-value": tbed_val
666 elif trigger_id == "ri-cadences":
667 tbed_opts = self._generate_options(
668 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
669 ctrl_panel.get("ri-ttypes-value"), cadence))
670 tbed_val = tbed_opts[0]["value"]
672 "ri-cadences-value": cadence,
673 "dd-tbeds-options": tbed_opts,
674 "dd-tbeds-value": tbed_val
676 elif trigger_id == "dd-tbeds":
678 "dd-tbeds-value": tbed
680 elif trigger_id == "dpr-period":
682 elif trigger_id == "url":
683 # TODO: Add verification
685 new_job = url_params.get("job", list())[0]
686 new_start = url_params.get("start", list())[0]
687 new_end = url_params.get("end", list())[0]
688 if new_job and new_start and new_end:
689 start = self._get_date(new_start)
690 end = self._get_date(new_end)
691 job_params = self._set_job_params(new_job)
692 ctrl_panel = self.ControlPanel(None, job_params)
694 ctrl_panel = self.ControlPanel(cp_data, self.default)
696 ctrl_panel.get("ri-duts-value"),
697 ctrl_panel.get("ri-ttypes-value"),
698 ctrl_panel.get("ri-cadences-value"),
699 ctrl_panel.get("dd-tbeds-value")
703 ctrl_panel.get("ri-duts-value"),
704 ctrl_panel.get("ri-ttypes-value"),
705 ctrl_panel.get("ri-cadences-value"),
706 ctrl_panel.get("dd-tbeds-value")
709 ctrl_panel.set({"al-job-children": job})
710 fig_passed, fig_duration = graph_statistics(self.data, job,
711 self.layout, start, end)
726 ret_val.extend(ctrl_panel.values())
730 Output("download-data", "data"),
731 State("control-panel", "data"), # Store
732 State("dpr-period", "start_date"),
733 State("dpr-period", "end_date"),
734 Input("btn-download-data", "n_clicks"),
735 prevent_initial_call=True
737 def _download_data(cp_data: dict, start: str, end: str, n_clicks: int):
743 ctrl_panel = self.ControlPanel(cp_data, self.default)
746 ctrl_panel.get("ri-duts-value"),
747 ctrl_panel.get("ri-ttypes-value"),
748 ctrl_panel.get("ri-cadences-value"),
749 ctrl_panel.get("dd-tbeds-value")
752 start = datetime(int(start[0:4]), int(start[5:7]), int(start[8:10]))
753 end = datetime(int(end[0:4]), int(end[5:7]), int(end[8:10]))
754 data = select_data(self.data, job, start, end)
755 data = data.drop(columns=["job", ])
757 return dcc.send_data_frame(data.T.to_csv, f"{job}-stats.csv")
760 Output("row-metadata", "children"),
761 Output("offcanvas-metadata", "is_open"),
762 Input("graph-passed", "clickData"),
763 Input("graph-duration", "clickData"),
764 prevent_initial_call=True
766 def _show_metadata_from_graphs(
767 passed_data: dict, duration_data: dict) -> tuple:
771 if not (passed_data or duration_data):
776 title = "Job Statistics"
777 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
778 if trigger_id == "graph-passed":
779 graph_data = passed_data["points"][0].get("hovertext", "")
780 elif trigger_id == "graph-duration":
781 graph_data = duration_data["points"][0].get("text", "")
783 lst_graph_data = graph_data.split("<br>")
785 # Prepare list of failed tests:
788 for itm in lst_graph_data:
789 if "csit-ref:" in itm:
790 job, build = itm.split(" ")[-1].split("/")
793 fail_tests = self.data.loc[
794 (self.data["job"] == job) &
795 (self.data["build"] == build)
796 ]["lst_failed"].values[0]
802 # Create the content of the offcanvas:
805 class_name="gy-2 p-0",
807 dbc.CardHeader(children=[
809 target_id="metadata",
811 style={"display": "inline-block"}
818 children=[dbc.ListGroup(
827 ) for x in lst_graph_data
836 if fail_tests is not None:
839 class_name="gy-2 p-0",
842 f"List of Failed Tests ({len(fail_tests)})"
847 children=[dbc.ListGroup(
849 dbc.ListGroupItem(x) \
861 return metadata, open_canvas