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
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 _show_tooltip(self, id: str, title: str,
393 clipboard_id: str=None) -> list:
394 """Generate list of elements to display a text (e.g. a title) with a
395 tooltip and optionaly with Copy&Paste icon and the clipboard
396 functionality enabled.
398 :param id: Tooltip ID.
399 :param title: A text for which the tooltip will be displayed.
400 :param clipboard_id: If defined, a Copy&Paste icon is displayed and the
401 clipboard functionality is enabled.
404 :type clipboard_id: str
405 :returns: List of elements to display a text with a tooltip and
406 optionaly with Copy&Paste icon.
411 dcc.Clipboard(target_id=clipboard_id, title="Copy URL") \
412 if clipboard_id else str(),
420 class_name="border ms-1",
423 children=self._tooltips.get(id, str()),
429 def add_content(self):
430 """Top level method which generated the web page.
433 - Store for user input data,
435 - Main area with control panel and ploting area.
437 If no HTML layout is provided, an error message is displayed instead.
439 :returns: The HTML div with teh whole page.
447 dcc.Store(id="control-panel"),
459 self._add_ctrl_col(),
460 self._add_plotting_col(),
478 def _add_navbar(self):
479 """Add nav element with navigation panel. It is placed on the top.
481 :returns: Navigation bar.
482 :rtype: dbc.NavbarSimple
485 return dbc.NavbarSimple(
486 id="navbarsimple-main",
490 "Continuous Performance News",
499 brand_external_link=True,
504 def _add_ctrl_col(self) -> dbc.Col:
505 """Add column with control panel. It is placed on the left side.
507 :returns: Column with the control panel.
514 self._add_ctrl_panel(),
518 def _add_plotting_col(self) -> dbc.Col:
519 """Add column with tables. It is placed on the right side.
521 :returns: Column with tables.
526 id="col-plotting-area",
528 dbc.Row( # Failed tests
529 id="row-table-failed",
530 class_name="g-0 p-2",
531 children=self._default_tab_failed
537 def _add_ctrl_panel(self) -> dbc.Row:
538 """Add control panel.
540 :returns: Control panel.
548 class_name="g-0 p-2",
555 children=self._show_tooltip(
556 "help-dut", "Device under Test")
562 value=self.default["dut"],
563 options=self.default["duts"]
573 children=self._show_tooltip(
574 "help-ttype", "Test Type"),
579 value=self.default["ttype"],
580 options=self.default["ttypes"]
589 children=self._show_tooltip(
590 "help-cadence", "Cadence"),
595 value=self.default["cadence"],
596 options=self.default["cadences"]
605 children=self._show_tooltip(
606 "help-tbed", "Test Bed"),
610 placeholder="Select a test bed...",
611 value=self.default["tbed"],
612 options=self.default["tbeds"]
622 children=self.default["job"]
635 def __init__(self, panel: dict, default: dict) -> None:
640 "ri-ttypes-options": default["ttypes"],
641 "ri-cadences-options": default["cadences"],
642 "dd-tbeds-options": default["tbeds"],
643 "ri-duts-value": default["dut"],
644 "ri-ttypes-value": default["ttype"],
645 "ri-cadences-value": default["cadence"],
646 "dd-tbeds-value": default["tbed"],
647 "al-job-children": default["job"]
649 self._panel = deepcopy(self._defaults)
651 for key in self._defaults:
652 self._panel[key] = panel[key]
654 def set(self, kwargs: dict) -> None:
655 for key, val in kwargs.items():
656 if key in self._panel:
657 self._panel[key] = val
659 raise KeyError(f"The key {key} is not defined.")
662 def defaults(self) -> dict:
663 return self._defaults
666 def panel(self) -> dict:
669 def get(self, key: str) -> any:
670 return self._panel[key]
672 def values(self) -> list:
673 return list(self._panel.values())
675 def callbacks(self, app):
678 Output("control-panel", "data"), # Store
679 Output("row-table-failed", "children"),
680 Output("ri-ttypes", "options"),
681 Output("ri-cadences", "options"),
682 Output("dd-tbeds", "options"),
683 Output("ri-duts", "value"),
684 Output("ri-ttypes", "value"),
685 Output("ri-cadences", "value"),
686 Output("dd-tbeds", "value"),
687 Output("al-job", "children"),
688 State("control-panel", "data"), # Store
689 Input("ri-duts", "value"),
690 Input("ri-ttypes", "value"),
691 Input("ri-cadences", "value"),
692 Input("dd-tbeds", "value"),
694 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
699 ctrl_panel = self.ControlPanel(cp_data, self.default)
701 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
702 if trigger_id == "ri-duts":
703 ttype_opts = self._generate_options(self._get_ttypes(dut))
704 ttype_val = ttype_opts[0]["value"]
705 cad_opts = self._generate_options(
706 self._get_cadences(dut, ttype_val))
707 cad_val = cad_opts[0]["value"]
708 tbed_opts = self._generate_options(
709 self._get_test_beds(dut, ttype_val, cad_val))
710 tbed_val = tbed_opts[0]["value"]
712 "ri-duts-value": dut,
713 "ri-ttypes-options": ttype_opts,
714 "ri-ttypes-value": ttype_val,
715 "ri-cadences-options": cad_opts,
716 "ri-cadences-value": cad_val,
717 "dd-tbeds-options": tbed_opts,
718 "dd-tbeds-value": tbed_val
720 elif trigger_id == "ri-ttypes":
721 cad_opts = self._generate_options(
722 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
723 cad_val = cad_opts[0]["value"]
724 tbed_opts = self._generate_options(
725 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
727 tbed_val = tbed_opts[0]["value"]
729 "ri-ttypes-value": ttype,
730 "ri-cadences-options": cad_opts,
731 "ri-cadences-value": cad_val,
732 "dd-tbeds-options": tbed_opts,
733 "dd-tbeds-value": tbed_val
735 elif trigger_id == "ri-cadences":
736 tbed_opts = self._generate_options(
737 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
738 ctrl_panel.get("ri-ttypes-value"), cadence))
739 tbed_val = tbed_opts[0]["value"]
741 "ri-cadences-value": cadence,
742 "dd-tbeds-options": tbed_opts,
743 "dd-tbeds-value": tbed_val
745 elif trigger_id == "dd-tbeds":
747 "dd-tbeds-value": tbed
751 ctrl_panel.get("ri-duts-value"),
752 ctrl_panel.get("ri-ttypes-value"),
753 ctrl_panel.get("ri-cadences-value"),
754 ctrl_panel.get("dd-tbeds-value")
756 ctrl_panel.set({"al-job-children": job})
757 tab_failed = table_news(self.data, job)
763 ret_val.extend(ctrl_panel.values())