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"
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:
581 return [{"label": i, "value": i} for i in opts]
584 def _get_date(s_date: str) -> datetime:
585 return datetime(int(s_date[0:4]), int(s_date[5:7]), int(s_date[8:10]))
587 def callbacks(self, app):
590 Output("control-panel", "data"), # Store
591 Output("graph-passed", "figure"),
592 Output("graph-duration", "figure"),
593 Output("card-url", "children"),
594 Output("ri-ttypes", "options"),
595 Output("ri-cadences", "options"),
596 Output("dd-tbeds", "options"),
597 Output("ri-duts", "value"),
598 Output("ri-ttypes", "value"),
599 Output("ri-cadences", "value"),
600 Output("dd-tbeds", "value"),
601 Output("al-job", "children"),
602 State("control-panel", "data"), # Store
603 Input("ri-duts", "value"),
604 Input("ri-ttypes", "value"),
605 Input("ri-cadences", "value"),
606 Input("dd-tbeds", "value"),
607 Input("dpr-period", "start_date"),
608 Input("dpr-period", "end_date"),
610 # prevent_initial_call=True
612 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
613 tbed: str, start: str, end: str, href: str) -> tuple:
617 ctrl_panel = self.ControlPanel(cp_data, self.default)
619 start = self._get_date(start)
620 end = self._get_date(end)
623 parsed_url = url_decode(href)
625 url_params = parsed_url["params"]
629 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
630 if trigger_id == "ri-duts":
631 ttype_opts = self._generate_options(self._get_ttypes(dut))
632 ttype_val = ttype_opts[0]["value"]
633 cad_opts = self._generate_options(
634 self._get_cadences(dut, ttype_val))
635 cad_val = cad_opts[0]["value"]
636 tbed_opts = self._generate_options(
637 self._get_test_beds(dut, ttype_val, cad_val))
638 tbed_val = tbed_opts[0]["value"]
640 "ri-duts-value": dut,
641 "ri-ttypes-options": ttype_opts,
642 "ri-ttypes-value": ttype_val,
643 "ri-cadences-options": cad_opts,
644 "ri-cadences-value": cad_val,
645 "dd-tbeds-options": tbed_opts,
646 "dd-tbeds-value": tbed_val
648 elif trigger_id == "ri-ttypes":
649 cad_opts = self._generate_options(
650 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
651 cad_val = cad_opts[0]["value"]
652 tbed_opts = self._generate_options(
653 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
655 tbed_val = tbed_opts[0]["value"]
657 "ri-ttypes-value": ttype,
658 "ri-cadences-options": cad_opts,
659 "ri-cadences-value": cad_val,
660 "dd-tbeds-options": tbed_opts,
661 "dd-tbeds-value": tbed_val
663 elif trigger_id == "ri-cadences":
664 tbed_opts = self._generate_options(
665 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
666 ctrl_panel.get("ri-ttypes-value"), cadence))
667 tbed_val = tbed_opts[0]["value"]
669 "ri-cadences-value": cadence,
670 "dd-tbeds-options": tbed_opts,
671 "dd-tbeds-value": tbed_val
673 elif trigger_id == "dd-tbeds":
675 "dd-tbeds-value": tbed
677 elif trigger_id == "dpr-period":
679 elif trigger_id == "url":
680 # TODO: Add verification
682 new_job = url_params.get("job", list())[0]
683 new_start = url_params.get("start", list())[0]
684 new_end = url_params.get("end", list())[0]
685 if new_job and new_start and new_end:
686 start = self._get_date(new_start)
687 end = self._get_date(new_end)
688 job_params = self._set_job_params(new_job)
689 ctrl_panel = self.ControlPanel(None, job_params)
691 ctrl_panel = self.ControlPanel(cp_data, self.default)
693 ctrl_panel.get("ri-duts-value"),
694 ctrl_panel.get("ri-ttypes-value"),
695 ctrl_panel.get("ri-cadences-value"),
696 ctrl_panel.get("dd-tbeds-value")
700 ctrl_panel.get("ri-duts-value"),
701 ctrl_panel.get("ri-ttypes-value"),
702 ctrl_panel.get("ri-cadences-value"),
703 ctrl_panel.get("dd-tbeds-value")
706 ctrl_panel.set({"al-job-children": job})
707 fig_passed, fig_duration = graph_statistics(self.data, job,
708 self.layout, start, end)
711 new_url = url_encode({
712 "scheme": parsed_url["scheme"],
713 "netloc": parsed_url["netloc"],
714 "path": parsed_url["path"],
730 target_id="card-url",
732 style={"display": "inline-block"}
737 ret_val.extend(ctrl_panel.values())
741 Output("download-data", "data"),
742 State("control-panel", "data"), # Store
743 State("dpr-period", "start_date"),
744 State("dpr-period", "end_date"),
745 Input("btn-download-data", "n_clicks"),
746 prevent_initial_call=True
748 def _download_data(cp_data: dict, start: str, end: str, n_clicks: int):
754 ctrl_panel = self.ControlPanel(cp_data, self.default)
757 ctrl_panel.get("ri-duts-value"),
758 ctrl_panel.get("ri-ttypes-value"),
759 ctrl_panel.get("ri-cadences-value"),
760 ctrl_panel.get("dd-tbeds-value")
763 start = datetime(int(start[0:4]), int(start[5:7]), int(start[8:10]))
764 end = datetime(int(end[0:4]), int(end[5:7]), int(end[8:10]))
765 data = select_data(self.data, job, start, end)
766 data = data.drop(columns=["job", ])
768 return dcc.send_data_frame(data.T.to_csv, f"{job}-stats.csv")
771 Output("row-metadata", "children"),
772 Output("offcanvas-metadata", "is_open"),
773 Input("graph-passed", "clickData"),
774 Input("graph-duration", "clickData"),
775 prevent_initial_call=True
777 def _show_metadata_from_graphs(
778 passed_data: dict, duration_data: dict) -> tuple:
782 if not (passed_data or duration_data):
787 title = "Job Statistics"
788 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
789 if trigger_id == "graph-passed":
790 graph_data = passed_data["points"][0].get("hovertext", "")
791 elif trigger_id == "graph-duration":
792 graph_data = duration_data["points"][0].get("text", "")
796 class_name="gy-2 p-0",
798 dbc.CardHeader(children=[
800 target_id="metadata",
802 style={"display": "inline-block"}
809 children=[dbc.ListGroup(
818 ) for x in graph_data.split("<br>")
828 return metadata, open_canvas