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.
20 import dash_bootstrap_components as dbc
22 from flask import Flask
25 from dash import callback_context, no_update
26 from dash import Input, Output, State
27 from dash.exceptions import PreventUpdate
28 from yaml import load, FullLoader, YAMLError
29 from datetime import datetime, timedelta
30 from copy import deepcopy
32 from ..data.data import Data
33 from .graphs import graph_statistics, select_data
40 DEFAULT_JOB = "csit-vpp-perf-mrr-daily-master-2n-icx"
42 def __init__(self, app: Flask, html_layout_file: str, spec_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._spec_file = spec_file
52 self._graph_layout_file = graph_layout_file
53 self._data_spec_file = data_spec_file
54 self._tooltip_file = tooltip_file
55 self._time_period = time_period
58 data_stats, data_mrr, data_ndrpdr = Data(
59 data_spec_file=self._data_spec_file,
61 ).read_stats(days=self._time_period)
63 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
65 # Pre-process the data:
66 data_stats = data_stats[~data_stats.job.str.contains("-verify-")]
67 data_stats = data_stats[~data_stats.job.str.contains("-coverage-")]
68 data_stats = data_stats[~data_stats.job.str.contains("-iterative-")]
69 data_stats = data_stats[["job", "build", "start_time", "duration"]]
72 (datetime.utcnow() - data_stats["start_time"].min()).days
73 if self._time_period > data_time_period:
74 self._time_period = data_time_period
76 jobs = sorted(list(data_stats["job"].unique()))
85 lst_job = job.split("-")
86 job_info["job"].append(job)
87 job_info["dut"].append(lst_job[1])
88 job_info["ttype"].append(lst_job[3])
89 job_info["cadence"].append(lst_job[4])
90 job_info["tbed"].append("-".join(lst_job[-2:]))
91 self.df_job_info = pd.DataFrame.from_dict(job_info)
93 self._default = self._set_job_params(self.DEFAULT_JOB)
99 "dut_version": list(),
105 # TODO: Add list of failed tests for each build
106 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
107 builds = df_job["build"].unique()
109 df_build = df_job.loc[(df_job["build"] == build)]
110 tst_info["job"].append(job)
111 tst_info["build"].append(build)
112 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
113 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
114 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
116 passed = df_build.value_counts(subset='passed')[True]
120 failed = df_build.value_counts(subset='passed')[False]
123 tst_info["passed"].append(passed)
124 tst_info["failed"].append(failed)
126 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
129 self._html_layout = ""
130 self._graph_layout = None
131 self._tooltips = dict()
134 with open(self._html_layout_file, "r") as file_read:
135 self._html_layout = file_read.read()
136 except IOError as err:
138 f"Not possible to open the file {self._html_layout_file}\n{err}"
142 with open(self._graph_layout_file, "r") as file_read:
143 self._graph_layout = load(file_read, Loader=FullLoader)
144 except IOError as err:
146 f"Not possible to open the file {self._graph_layout_file}\n"
149 except YAMLError as err:
151 f"An error occurred while parsing the specification file "
152 f"{self._graph_layout_file}\n{err}"
156 with open(self._tooltip_file, "r") as file_read:
157 self._tooltips = load(file_read, Loader=FullLoader)
158 except IOError as err:
160 f"Not possible to open the file {self._tooltip_file}\n{err}"
162 except YAMLError as err:
164 f"An error occurred while parsing the specification file "
165 f"{self._tooltip_file}\n{err}"
169 self._default_fig_passed, self._default_fig_duration = graph_statistics(
170 self.data, self._default["job"], self.layout
174 if self._app is not None and hasattr(self, 'callbacks'):
175 self.callbacks(self._app)
178 def html_layout(self) -> dict:
179 return self._html_layout
182 def data(self) -> pd.DataFrame:
186 def layout(self) -> dict:
187 return self._graph_layout
190 def time_period(self) -> int:
191 return self._time_period
194 def default(self) -> any:
197 def _get_duts(self) -> list:
200 return sorted(list(self.df_job_info["dut"].unique()))
202 def _get_ttypes(self, dut: str) -> list:
205 return sorted(list(self.df_job_info.loc[(
206 self.df_job_info["dut"] == dut
207 )]["ttype"].unique()))
209 def _get_cadences(self, dut: str, ttype: str) -> list:
212 return sorted(list(self.df_job_info.loc[(
213 (self.df_job_info["dut"] == dut) &
214 (self.df_job_info["ttype"] == ttype)
215 )]["cadence"].unique()))
217 def _get_test_beds(self, dut: str, ttype: str, cadence: 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 (self.df_job_info["cadence"] == cadence)
224 )]["tbed"].unique()))
226 def _get_job(self, dut, ttype, cadence, testbed):
227 """Get the name of a job defined by dut, ttype, cadence, testbed.
229 Input information comes from control panel.
231 return self.df_job_info.loc[(
232 (self.df_job_info["dut"] == dut) &
233 (self.df_job_info["ttype"] == ttype) &
234 (self.df_job_info["cadence"] == cadence) &
235 (self.df_job_info["tbed"] == testbed)
238 def _set_job_params(self, job: str) -> dict:
241 lst_job = job.split("-")
246 "cadence": lst_job[4],
247 "tbed": "-".join(lst_job[-2:]),
248 "duts": self._generate_options(self._get_duts()),
249 "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
250 "cadences": self._generate_options(self._get_cadences(
251 lst_job[1], lst_job[3])),
252 "tbeds": self._generate_options(self._get_test_beds(
253 lst_job[1], lst_job[3], lst_job[4]))
256 def _show_tooltip(self, id: str, title: str) -> list:
267 class_name="border ms-1",
270 children=self._tooltips.get(id, str()),
276 def add_content(self):
283 dcc.Store(id="control-panel"),
284 dcc.Location(id="url", refresh=False),
295 id="offcanvas-metadata",
296 title="Detailed Information",
300 dbc.Row(id="row-metadata")
308 self._add_ctrl_col(),
309 self._add_plotting_col(),
327 def _add_navbar(self):
328 """Add nav element with navigation panel. It is placed on the top.
330 return dbc.NavbarSimple(
331 id="navbarsimple-main",
335 "Continuous Performance Statistics",
344 brand_external_link=True,
349 def _add_ctrl_col(self) -> dbc.Col:
350 """Add column with controls. It is placed on the left side.
355 self._add_ctrl_panel(),
359 def _add_plotting_col(self) -> dbc.Col:
360 """Add column with plots and tables. It is placed on the right side.
363 id="col-plotting-area",
365 dbc.Row( # Passed / failed tests
366 id="row-graph-passed",
367 class_name="g-0 p-2",
369 dcc.Loading(children=[
372 figure=self._default_fig_passed
378 id="row-graph-duration",
379 class_name="g-0 p-2",
381 dcc.Loading(children=[
384 figure=self._default_fig_duration
390 class_name="g-0 p-2",
397 dcc.Loading(children=[
399 id="btn-download-data",
400 children=self._show_tooltip(
401 "help-download", "Download Data"),
405 dcc.Download(id="download-data")
415 class_name="gy-2 p-0",
426 def _add_ctrl_panel(self) -> dbc.Row:
434 class_name="g-0 p-2",
441 children=self._show_tooltip(
442 "help-dut", "Device under Test")
448 value=self.default["dut"],
449 options=self.default["duts"]
459 children=self._show_tooltip(
460 "help-ttype", "Test Type"),
465 value=self.default["ttype"],
466 options=self.default["ttypes"]
475 children=self._show_tooltip(
476 "help-cadence", "Cadence"),
481 value=self.default["cadence"],
482 options=self.default["cadences"]
491 children=self._show_tooltip(
492 "help-tbed", "Test Bed"),
496 placeholder="Select a test bed...",
497 value=self.default["tbed"],
498 options=self.default["tbeds"]
508 children=self.default["job"]
513 class_name="g-0 p-2",
517 children=self._show_tooltip(
518 "help-time-period", "Time Period"),
522 className="d-flex justify-content-center",
524 datetime.utcnow() - timedelta(
525 days=self.time_period),
526 max_date_allowed=datetime.utcnow(),
527 initial_visible_month=datetime.utcnow(),
529 datetime.utcnow() - timedelta(
530 days=self.time_period),
531 end_date=datetime.utcnow(),
532 display_format="D MMM YY"
542 def __init__(self, panel: dict, default: dict) -> None:
544 "ri-ttypes-options": default["ttypes"],
545 "ri-cadences-options": default["cadences"],
546 "dd-tbeds-options": default["tbeds"],
547 "ri-duts-value": default["dut"],
548 "ri-ttypes-value": default["ttype"],
549 "ri-cadences-value": default["cadence"],
550 "dd-tbeds-value": default["tbed"],
551 "al-job-children": default["job"]
553 self._panel = deepcopy(self._defaults)
555 for key in self._defaults:
556 self._panel[key] = panel[key]
558 def set(self, kwargs: dict) -> None:
559 for key, val in kwargs.items():
560 if key in self._panel:
561 self._panel[key] = val
563 raise KeyError(f"The key {key} is not defined.")
566 def defaults(self) -> dict:
567 return self._defaults
570 def panel(self) -> dict:
573 def get(self, key: str) -> any:
574 return self._panel[key]
576 def values(self) -> list:
577 return list(self._panel.values())
580 def _generate_options(opts: list) -> list:
583 return [{"label": i, "value": i} for i in opts]
585 def callbacks(self, app):
588 Output("control-panel", "data"), # Store
589 Output("graph-passed", "figure"),
590 Output("graph-duration", "figure"),
591 Output("card-url", "children"),
592 Output("ri-ttypes", "options"),
593 Output("ri-cadences", "options"),
594 Output("dd-tbeds", "options"),
595 Output("ri-duts", "value"),
596 Output("ri-ttypes", "value"),
597 Output("ri-cadences", "value"),
598 Output("dd-tbeds", "value"),
599 Output("al-job", "children"),
600 State("control-panel", "data"), # Store
601 Input("ri-duts", "value"),
602 Input("ri-ttypes", "value"),
603 Input("ri-cadences", "value"),
604 Input("dd-tbeds", "value"),
605 Input("dpr-period", "start_date"),
606 Input("dpr-period", "end_date"),
608 # prevent_initial_call=True
610 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
611 tbed: str, start: str, end: str, href: str) -> tuple:
615 ctrl_panel = self.ControlPanel(cp_data, self.default)
617 start = datetime(int(start[0:4]), int(start[5:7]), int(start[8:10]))
618 end = datetime(int(end[0:4]), int(end[5:7]), int(end[8:10]))
620 parsed_url = urllib.parse.urlparse(href)
621 url = f"{parsed_url.netloc}{parsed_url.path}"
622 url_params = urllib.parse.parse_qs(parsed_url.fragment)
624 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
625 if trigger_id == "ri-duts":
626 ttype_opts = self._generate_options(self._get_ttypes(dut))
627 ttype_val = ttype_opts[0]["value"]
628 cad_opts = self._generate_options(
629 self._get_cadences(dut, ttype_val))
630 cad_val = cad_opts[0]["value"]
631 tbed_opts = self._generate_options(
632 self._get_test_beds(dut, ttype_val, cad_val))
633 tbed_val = tbed_opts[0]["value"]
635 "ri-duts-value": dut,
636 "ri-ttypes-options": ttype_opts,
637 "ri-ttypes-value": ttype_val,
638 "ri-cadences-options": cad_opts,
639 "ri-cadences-value": cad_val,
640 "dd-tbeds-options": tbed_opts,
641 "dd-tbeds-value": tbed_val
643 elif trigger_id == "ri-ttypes":
644 cad_opts = self._generate_options(
645 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
646 cad_val = cad_opts[0]["value"]
647 tbed_opts = self._generate_options(
648 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
650 tbed_val = tbed_opts[0]["value"]
652 "ri-ttypes-value": ttype,
653 "ri-cadences-options": cad_opts,
654 "ri-cadences-value": cad_val,
655 "dd-tbeds-options": tbed_opts,
656 "dd-tbeds-value": tbed_val
658 elif trigger_id == "ri-cadences":
659 tbed_opts = self._generate_options(
660 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
661 ctrl_panel.get("ri-ttypes-value"), cadence))
662 tbed_val = tbed_opts[0]["value"]
664 "ri-cadences-value": cadence,
665 "dd-tbeds-options": tbed_opts,
666 "dd-tbeds-value": tbed_val
668 elif trigger_id == "dd-tbeds":
670 "dd-tbeds-value": tbed
672 elif trigger_id == "dpr-period":
674 elif trigger_id == "url":
675 # TODO: Add verification
677 new_job = url_params.get("job", list())[0]
678 new_start = url_params.get("start", list())[0]
679 new_end = url_params.get("end", list())[0]
680 if new_job and new_start and new_end:
682 int(new_start[0:4]), int(new_start[5:7]),
683 int(new_start[8:10]))
685 int(new_end[0:4]), int(new_end[5:7]),
687 job_params = self._set_job_params(new_job)
688 ctrl_panel = self.ControlPanel(None, job_params)
690 ctrl_panel = self.ControlPanel(cp_data, self.default)
692 ctrl_panel.get("ri-duts-value"),
693 ctrl_panel.get("ri-ttypes-value"),
694 ctrl_panel.get("ri-cadences-value"),
695 ctrl_panel.get("dd-tbeds-value")
699 ctrl_panel.get("ri-duts-value"),
700 ctrl_panel.get("ri-ttypes-value"),
701 ctrl_panel.get("ri-cadences-value"),
702 ctrl_panel.get("dd-tbeds-value")
710 ctrl_panel.set({"al-job-children": job})
711 fig_passed, fig_duration = graph_statistics(
712 self.data, job, self.layout, start, end)
720 target_id="card-url",
722 style={"display": "inline-block"}
724 f"{url}#{urllib.parse.urlencode(url_params)}"
727 ret_val.extend(ctrl_panel.values())
731 Output("download-data", "data"),
732 State("control-panel", "data"), # Store
733 State("dpr-period", "start_date"),
734 State("dpr-period", "end_date"),
735 Input("btn-download-data", "n_clicks"),
736 prevent_initial_call=True
738 def _download_data(cp_data: dict, start: str, end: str, n_clicks: int):
744 ctrl_panel = self.ControlPanel(cp_data, self.default)
747 ctrl_panel.get("ri-duts-value"),
748 ctrl_panel.get("ri-ttypes-value"),
749 ctrl_panel.get("ri-cadences-value"),
750 ctrl_panel.get("dd-tbeds-value")
753 start = datetime(int(start[0:4]), int(start[5:7]), int(start[8:10]))
754 end = datetime(int(end[0:4]), int(end[5:7]), int(end[8:10]))
755 data = select_data(self.data, job, start, end)
756 data = data.drop(columns=["job", ])
758 return dcc.send_data_frame(data.T.to_csv, f"{job}-stats.csv")
761 Output("row-metadata", "children"),
762 Output("offcanvas-metadata", "is_open"),
763 Input("graph-passed", "clickData"),
764 Input("graph-duration", "clickData"),
765 prevent_initial_call=True
767 def _show_metadata_from_graphs(
768 passed_data: dict, duration_data: dict) -> tuple:
772 if not (passed_data or duration_data):
777 title = "Job Statistics"
778 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
779 if trigger_id == "graph-passed":
780 graph_data = passed_data["points"][0].get("hovertext", "")
781 elif trigger_id == "graph-duration":
782 graph_data = duration_data["points"][0].get("text", "")
786 class_name="gy-2 p-0",
788 dbc.CardHeader(children=[
790 target_id="metadata",
792 style={"display": "inline-block"}
799 children=[dbc.ListGroup(
808 ) for x in graph_data.split("<br>")
818 return metadata, open_canvas