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
25 from dash import Input, Output, State
26 from yaml import load, FullLoader, YAMLError
27 from copy import deepcopy
29 from ..data.data import Data
30 from ..utils.constants import Constants as C
31 from ..utils.utils import classify_anomalies, show_tooltip
32 from ..data.data import Data
33 from .tables import table_news
37 """The layout of the dash app and the callbacks.
40 def __init__(self, app: Flask, html_layout_file: str, data_spec_file: str,
41 tooltip_file: str) -> None:
43 - save the input parameters,
44 - read and pre-process the data,
45 - prepare data fro the control panel,
46 - read HTML layout file,
47 - read tooltips from the tooltip file.
49 :param app: Flask application running the dash application.
50 :param html_layout_file: Path and name of the file specifying the HTML
51 layout of the dash application.
52 :param data_spec_file: Path and name of the file specifying the data to
53 be read from parquets for this application.
54 :param tooltip_file: Path and name of the yaml file specifying the
57 :type html_layout_file: str
58 :type data_spec_file: str
59 :type tooltip_file: str
64 self._html_layout_file = html_layout_file
65 self._data_spec_file = data_spec_file
66 self._tooltip_file = tooltip_file
69 data_stats, data_mrr, data_ndrpdr = Data(
70 data_spec_file=self._data_spec_file,
72 ).read_stats(days=C.NEWS_TIME_PERIOD)
74 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
76 # Prepare information for the control panel:
77 jobs = sorted(list(df_tst_info["job"].unique()))
86 lst_job = job.split("-")
87 job_info["job"].append(job)
88 job_info["dut"].append(lst_job[1])
89 job_info["ttype"].append(lst_job[3])
90 job_info["cadence"].append(lst_job[4])
91 job_info["tbed"].append("-".join(lst_job[-2:]))
92 self.df_job_info = pd.DataFrame.from_dict(job_info)
94 self._default = self._set_job_params(C.NEWS_DEFAULT_JOB)
96 # Pre-process the data:
98 def _create_test_name(test: str) -> str:
99 lst_tst = test.split(".")
100 suite = lst_tst[-2].replace("2n1l-", "").replace("1n1l-", "").\
102 return f"{suite.split('-')[0]}-{lst_tst[-1]}"
104 def _get_rindex(array: list, itm: any) -> int:
105 return len(array) - 1 - array[::-1].index(itm)
112 "dut_version": list(),
115 "regressions": list(),
116 "progressions": list()
119 # Create lists of failed tests:
120 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
121 last_build = max(df_job["build"].unique())
122 df_build = df_job.loc[(df_job["build"] == last_build)]
123 tst_info["job"].append(job)
124 tst_info["build"].append(last_build)
125 tst_info["start"].append(data_stats.loc[
126 (data_stats["job"] == job) &
127 (data_stats["build"] == last_build)
128 ]["start_time"].iloc[-1].strftime('%Y-%m-%d %H:%M'))
129 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
130 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
131 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
132 failed_tests = df_build.loc[(df_build["passed"] == False)]\
133 ["test_id"].to_list()
136 for tst in failed_tests:
137 l_failed.append(_create_test_name(tst))
140 tst_info["failed"].append(sorted(l_failed))
142 # Create lists of regressions and progressions:
146 tests = df_job["test_id"].unique()
148 tst_data = df_job.loc[df_job["test_id"] == test].sort_values(
149 by="start_time", ignore_index=True)
150 x_axis = tst_data["start_time"].tolist()
151 if "-ndrpdr" in test:
152 tst_data = tst_data.dropna(
153 subset=["result_pdr_lower_rate_value", ]
158 anomalies, _, _ = classify_anomalies({
159 k: v for k, v in zip(
161 tst_data["result_ndr_lower_rate_value"].tolist()
166 if "progression" in anomalies:
168 _create_test_name(test).replace("-ndrpdr", "-ndr"),
169 x_axis[_get_rindex(anomalies, "progression")]
171 if "regression" in anomalies:
173 _create_test_name(test).replace("-ndrpdr", "-ndr"),
174 x_axis[_get_rindex(anomalies, "regression")]
177 anomalies, _, _ = classify_anomalies({
178 k: v for k, v in zip(
180 tst_data["result_pdr_lower_rate_value"].tolist()
185 if "progression" in anomalies:
187 _create_test_name(test).replace("-ndrpdr", "-pdr"),
188 x_axis[_get_rindex(anomalies, "progression")]
190 if "regression" in anomalies:
192 _create_test_name(test).replace("-ndrpdr", "-pdr"),
193 x_axis[_get_rindex(anomalies, "regression")]
196 tst_data = tst_data.dropna(
197 subset=["result_receive_rate_rate_avg", ]
202 anomalies, _, _ = classify_anomalies({
203 k: v for k, v in zip(
205 tst_data["result_receive_rate_rate_avg"].\
211 if "progression" in anomalies:
213 _create_test_name(test),
214 x_axis[_get_rindex(anomalies, "progression")]
216 if "regression" in anomalies:
218 _create_test_name(test),
219 x_axis[_get_rindex(anomalies, "regression")]
222 tst_info["regressions"].append(
223 sorted(l_reg, key=lambda k: k[1], reverse=True))
224 tst_info["progressions"].append(
225 sorted(l_prog, key=lambda k: k[1], reverse=True))
227 self._data = pd.DataFrame.from_dict(tst_info)
230 self._html_layout = str()
231 self._tooltips = dict()
234 with open(self._html_layout_file, "r") as file_read:
235 self._html_layout = file_read.read()
236 except IOError as err:
238 f"Not possible to open the file {self._html_layout_file}\n{err}"
242 with open(self._tooltip_file, "r") as file_read:
243 self._tooltips = load(file_read, Loader=FullLoader)
244 except IOError as err:
246 f"Not possible to open the file {self._tooltip_file}\n{err}"
248 except YAMLError as err:
250 f"An error occurred while parsing the specification file "
251 f"{self._tooltip_file}\n{err}"
254 self._default_tab_failed = table_news(self.data, self._default["job"])
257 if self._app is not None and hasattr(self, 'callbacks'):
258 self.callbacks(self._app)
261 def html_layout(self) -> dict:
262 return self._html_layout
265 def data(self) -> pd.DataFrame:
269 def default(self) -> dict:
272 def _get_duts(self) -> list:
273 """Get the list of DUTs from the pre-processed information about jobs.
275 :returns: Alphabeticaly sorted list of DUTs.
278 return sorted(list(self.df_job_info["dut"].unique()))
280 def _get_ttypes(self, dut: str) -> list:
281 """Get the list of test types from the pre-processed information about
284 :param dut: The DUT for which the list of test types will be populated.
286 :returns: Alphabeticaly sorted list of test types.
289 return sorted(list(self.df_job_info.loc[(
290 self.df_job_info["dut"] == dut
291 )]["ttype"].unique()))
293 def _get_cadences(self, dut: str, ttype: str) -> list:
294 """Get the list of cadences from the pre-processed information about
297 :param dut: The DUT for which the list of cadences will be populated.
298 :param ttype: The test type for which the list of cadences will be
302 :returns: Alphabeticaly sorted list of cadences.
305 return sorted(list(self.df_job_info.loc[(
306 (self.df_job_info["dut"] == dut) &
307 (self.df_job_info["ttype"] == ttype)
308 )]["cadence"].unique()))
310 def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list:
311 """Get the list of test beds from the pre-processed information about
314 :param dut: The DUT for which the list of test beds will be populated.
315 :param ttype: The test type for which the list of test beds will be
317 :param cadence: The cadence for which the list of test beds will be
322 :returns: Alphabeticaly sorted list of test beds.
325 return sorted(list(self.df_job_info.loc[(
326 (self.df_job_info["dut"] == dut) &
327 (self.df_job_info["ttype"] == ttype) &
328 (self.df_job_info["cadence"] == cadence)
329 )]["tbed"].unique()))
331 def _get_job(self, dut, ttype, cadence, testbed):
332 """Get the name of a job defined by dut, ttype, cadence, test bed.
333 Input information comes from the control panel.
335 :param dut: The DUT for which the job name will be created.
336 :param ttype: The test type for which the job name will be created.
337 :param cadence: The cadence for which the job name will be created.
338 :param testbed: The test bed for which the job name will be created.
346 return self.df_job_info.loc[(
347 (self.df_job_info["dut"] == dut) &
348 (self.df_job_info["ttype"] == ttype) &
349 (self.df_job_info["cadence"] == cadence) &
350 (self.df_job_info["tbed"] == testbed)
354 def _generate_options(opts: list) -> list:
355 """Return list of options for radio items in control panel. The items in
356 the list are dictionaries with keys "label" and "value".
358 :params opts: List of options (str) to be used for the generated list.
360 :returns: List of options (dict).
363 return [{"label": i, "value": i} for i in opts]
365 def _set_job_params(self, job: str) -> dict:
366 """Create a dictionary with all options and values for (and from) the
369 :params job: The name of job for and from which the dictionary will be
372 :returns: Dictionary with all options and values for (and from) the
377 lst_job = job.split("-")
382 "cadence": lst_job[4],
383 "tbed": "-".join(lst_job[-2:]),
384 "duts": self._generate_options(self._get_duts()),
385 "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
386 "cadences": self._generate_options(self._get_cadences(
387 lst_job[1], lst_job[3])),
388 "tbeds": self._generate_options(self._get_test_beds(
389 lst_job[1], lst_job[3], lst_job[4]))
392 def add_content(self):
393 """Top level method which generated the web page.
396 - Store for user input data,
398 - Main area with control panel and ploting area.
400 If no HTML layout is provided, an error message is displayed instead.
402 :returns: The HTML div with teh whole page.
410 dcc.Store(id="control-panel"),
422 self._add_ctrl_col(),
423 self._add_plotting_col(),
441 def _add_navbar(self):
442 """Add nav element with navigation panel. It is placed on the top.
444 :returns: Navigation bar.
445 :rtype: dbc.NavbarSimple
448 return dbc.NavbarSimple(
449 id="navbarsimple-main",
453 "Continuous Performance News",
462 brand_external_link=True,
467 def _add_ctrl_col(self) -> dbc.Col:
468 """Add column with control panel. It is placed on the left side.
470 :returns: Column with the control panel.
477 self._add_ctrl_panel(),
481 def _add_plotting_col(self) -> dbc.Col:
482 """Add column with tables. It is placed on the right side.
484 :returns: Column with tables.
489 id="col-plotting-area",
491 dbc.Row( # Failed tests
492 id="row-table-failed",
493 class_name="g-0 p-2",
494 children=self._default_tab_failed
500 def _add_ctrl_panel(self) -> dbc.Row:
501 """Add control panel.
503 :returns: Control panel.
511 class_name="g-0 p-2",
518 children=show_tooltip(self._tooltips,
519 "help-dut", "Device under Test")
525 value=self.default["dut"],
526 options=self.default["duts"]
536 children=show_tooltip(self._tooltips,
537 "help-ttype", "Test Type"),
542 value=self.default["ttype"],
543 options=self.default["ttypes"]
552 children=show_tooltip(self._tooltips,
553 "help-cadence", "Cadence"),
558 value=self.default["cadence"],
559 options=self.default["cadences"]
568 children=show_tooltip(self._tooltips,
569 "help-tbed", "Test Bed"),
573 placeholder="Select a test bed...",
574 value=self.default["tbed"],
575 options=self.default["tbeds"]
585 children=self.default["job"]
598 def __init__(self, panel: dict, default: dict) -> None:
603 "ri-ttypes-options": default["ttypes"],
604 "ri-cadences-options": default["cadences"],
605 "dd-tbeds-options": default["tbeds"],
606 "ri-duts-value": default["dut"],
607 "ri-ttypes-value": default["ttype"],
608 "ri-cadences-value": default["cadence"],
609 "dd-tbeds-value": default["tbed"],
610 "al-job-children": default["job"]
612 self._panel = deepcopy(self._defaults)
614 for key in self._defaults:
615 self._panel[key] = panel[key]
617 def set(self, kwargs: dict) -> None:
618 for key, val in kwargs.items():
619 if key in self._panel:
620 self._panel[key] = val
622 raise KeyError(f"The key {key} is not defined.")
625 def defaults(self) -> dict:
626 return self._defaults
629 def panel(self) -> dict:
632 def get(self, key: str) -> any:
633 return self._panel[key]
635 def values(self) -> list:
636 return list(self._panel.values())
638 def callbacks(self, app):
641 Output("control-panel", "data"), # Store
642 Output("row-table-failed", "children"),
643 Output("ri-ttypes", "options"),
644 Output("ri-cadences", "options"),
645 Output("dd-tbeds", "options"),
646 Output("ri-duts", "value"),
647 Output("ri-ttypes", "value"),
648 Output("ri-cadences", "value"),
649 Output("dd-tbeds", "value"),
650 Output("al-job", "children"),
651 State("control-panel", "data"), # Store
652 Input("ri-duts", "value"),
653 Input("ri-ttypes", "value"),
654 Input("ri-cadences", "value"),
655 Input("dd-tbeds", "value"),
657 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
662 ctrl_panel = self.ControlPanel(cp_data, self.default)
664 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
665 if trigger_id == "ri-duts":
666 ttype_opts = self._generate_options(self._get_ttypes(dut))
667 ttype_val = ttype_opts[0]["value"]
668 cad_opts = self._generate_options(
669 self._get_cadences(dut, ttype_val))
670 cad_val = cad_opts[0]["value"]
671 tbed_opts = self._generate_options(
672 self._get_test_beds(dut, ttype_val, cad_val))
673 tbed_val = tbed_opts[0]["value"]
675 "ri-duts-value": dut,
676 "ri-ttypes-options": ttype_opts,
677 "ri-ttypes-value": ttype_val,
678 "ri-cadences-options": cad_opts,
679 "ri-cadences-value": cad_val,
680 "dd-tbeds-options": tbed_opts,
681 "dd-tbeds-value": tbed_val
683 elif trigger_id == "ri-ttypes":
684 cad_opts = self._generate_options(
685 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
686 cad_val = cad_opts[0]["value"]
687 tbed_opts = self._generate_options(
688 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
690 tbed_val = tbed_opts[0]["value"]
692 "ri-ttypes-value": ttype,
693 "ri-cadences-options": cad_opts,
694 "ri-cadences-value": cad_val,
695 "dd-tbeds-options": tbed_opts,
696 "dd-tbeds-value": tbed_val
698 elif trigger_id == "ri-cadences":
699 tbed_opts = self._generate_options(
700 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
701 ctrl_panel.get("ri-ttypes-value"), cadence))
702 tbed_val = tbed_opts[0]["value"]
704 "ri-cadences-value": cadence,
705 "dd-tbeds-options": tbed_opts,
706 "dd-tbeds-value": tbed_val
708 elif trigger_id == "dd-tbeds":
710 "dd-tbeds-value": tbed
714 ctrl_panel.get("ri-duts-value"),
715 ctrl_panel.get("ri-ttypes-value"),
716 ctrl_panel.get("ri-cadences-value"),
717 ctrl_panel.get("dd-tbeds-value")
719 ctrl_panel.set({"al-job-children": job})
720 tab_failed = table_news(self.data, job)
726 ret_val.extend(ctrl_panel.values())