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, gen_new_url
32 from ..utils.url_processing import url_decode
33 from ..data.data import Data
34 from .tables import table_news
38 """The layout of the dash app and the callbacks.
41 def __init__(self, app: Flask, html_layout_file: str, data_spec_file: str,
42 tooltip_file: str) -> None:
44 - save the input parameters,
45 - read and pre-process the data,
46 - prepare data fro the control panel,
47 - read HTML layout file,
48 - read tooltips from the tooltip file.
50 :param app: Flask application running the dash application.
51 :param html_layout_file: Path and name of the file specifying the HTML
52 layout of the dash application.
53 :param data_spec_file: Path and name of the file specifying the data to
54 be read from parquets for this application.
55 :param tooltip_file: Path and name of the yaml file specifying the
58 :type html_layout_file: str
59 :type data_spec_file: str
60 :type tooltip_file: str
65 self._html_layout_file = html_layout_file
66 self._data_spec_file = data_spec_file
67 self._tooltip_file = tooltip_file
70 data_stats, data_mrr, data_ndrpdr = Data(
71 data_spec_file=self._data_spec_file,
73 ).read_stats(days=C.NEWS_TIME_PERIOD)
75 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
77 # Prepare information for the control panel:
78 jobs = sorted(list(df_tst_info["job"].unique()))
87 lst_job = job.split("-")
88 job_info["job"].append(job)
89 job_info["dut"].append(lst_job[1])
90 job_info["ttype"].append(lst_job[3])
91 job_info["cadence"].append(lst_job[4])
92 job_info["tbed"].append("-".join(lst_job[-2:]))
93 self.df_job_info = pd.DataFrame.from_dict(job_info)
95 self._default = self._set_job_params(C.NEWS_DEFAULT_JOB)
97 # Pre-process the data:
99 def _create_test_name(test: str) -> str:
100 lst_tst = test.split(".")
101 suite = lst_tst[-2].replace("2n1l-", "").replace("1n1l-", "").\
103 return f"{suite.split('-')[0]}-{lst_tst[-1]}"
105 def _get_rindex(array: list, itm: any) -> int:
106 return len(array) - 1 - array[::-1].index(itm)
113 "dut_version": list(),
116 "regressions": list(),
117 "progressions": list()
120 # Create lists of failed tests:
121 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
122 last_build = max(df_job["build"].unique())
123 df_build = df_job.loc[(df_job["build"] == last_build)]
124 tst_info["job"].append(job)
125 tst_info["build"].append(last_build)
126 tst_info["start"].append(data_stats.loc[
127 (data_stats["job"] == job) &
128 (data_stats["build"] == last_build)
129 ]["start_time"].iloc[-1].strftime('%Y-%m-%d %H:%M'))
130 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
131 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
132 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
133 failed_tests = df_build.loc[(df_build["passed"] == False)]\
134 ["test_id"].to_list()
137 for tst in failed_tests:
138 l_failed.append(_create_test_name(tst))
141 tst_info["failed"].append(sorted(l_failed))
143 # Create lists of regressions and progressions:
147 tests = df_job["test_id"].unique()
149 tst_data = df_job.loc[df_job["test_id"] == test].sort_values(
150 by="start_time", ignore_index=True)
151 x_axis = tst_data["start_time"].tolist()
152 if "-ndrpdr" in test:
153 tst_data = tst_data.dropna(
154 subset=["result_pdr_lower_rate_value", ]
159 anomalies, _, _ = classify_anomalies({
160 k: v for k, v in zip(
162 tst_data["result_ndr_lower_rate_value"].tolist()
167 if "progression" in anomalies:
169 _create_test_name(test).replace("-ndrpdr", "-ndr"),
170 x_axis[_get_rindex(anomalies, "progression")]
172 if "regression" in anomalies:
174 _create_test_name(test).replace("-ndrpdr", "-ndr"),
175 x_axis[_get_rindex(anomalies, "regression")]
178 anomalies, _, _ = classify_anomalies({
179 k: v for k, v in zip(
181 tst_data["result_pdr_lower_rate_value"].tolist()
186 if "progression" in anomalies:
188 _create_test_name(test).replace("-ndrpdr", "-pdr"),
189 x_axis[_get_rindex(anomalies, "progression")]
191 if "regression" in anomalies:
193 _create_test_name(test).replace("-ndrpdr", "-pdr"),
194 x_axis[_get_rindex(anomalies, "regression")]
197 tst_data = tst_data.dropna(
198 subset=["result_receive_rate_rate_avg", ]
203 anomalies, _, _ = classify_anomalies({
204 k: v for k, v in zip(
206 tst_data["result_receive_rate_rate_avg"].\
212 if "progression" in anomalies:
214 _create_test_name(test),
215 x_axis[_get_rindex(anomalies, "progression")]
217 if "regression" in anomalies:
219 _create_test_name(test),
220 x_axis[_get_rindex(anomalies, "regression")]
223 tst_info["regressions"].append(
224 sorted(l_reg, key=lambda k: k[1], reverse=True))
225 tst_info["progressions"].append(
226 sorted(l_prog, key=lambda k: k[1], reverse=True))
228 self._data = pd.DataFrame.from_dict(tst_info)
231 self._html_layout = str()
232 self._tooltips = dict()
235 with open(self._html_layout_file, "r") as file_read:
236 self._html_layout = file_read.read()
237 except IOError as err:
239 f"Not possible to open the file {self._html_layout_file}\n{err}"
243 with open(self._tooltip_file, "r") as file_read:
244 self._tooltips = load(file_read, Loader=FullLoader)
245 except IOError as err:
247 f"Not possible to open the file {self._tooltip_file}\n{err}"
249 except YAMLError as err:
251 f"An error occurred while parsing the specification file "
252 f"{self._tooltip_file}\n{err}"
255 self._default_tab_failed = table_news(self.data, self._default["job"])
258 if self._app is not None and hasattr(self, 'callbacks'):
259 self.callbacks(self._app)
262 def html_layout(self) -> dict:
263 return self._html_layout
266 def data(self) -> pd.DataFrame:
270 def default(self) -> dict:
273 def _get_duts(self) -> list:
274 """Get the list of DUTs from the pre-processed information about jobs.
276 :returns: Alphabeticaly sorted list of DUTs.
279 return sorted(list(self.df_job_info["dut"].unique()))
281 def _get_ttypes(self, dut: str) -> list:
282 """Get the list of test types from the pre-processed information about
285 :param dut: The DUT for which the list of test types will be populated.
287 :returns: Alphabeticaly sorted list of test types.
290 return sorted(list(self.df_job_info.loc[(
291 self.df_job_info["dut"] == dut
292 )]["ttype"].unique()))
294 def _get_cadences(self, dut: str, ttype: str) -> list:
295 """Get the list of cadences from the pre-processed information about
298 :param dut: The DUT for which the list of cadences will be populated.
299 :param ttype: The test type for which the list of cadences will be
303 :returns: Alphabeticaly sorted list of cadences.
306 return sorted(list(self.df_job_info.loc[(
307 (self.df_job_info["dut"] == dut) &
308 (self.df_job_info["ttype"] == ttype)
309 )]["cadence"].unique()))
311 def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list:
312 """Get the list of test beds from the pre-processed information about
315 :param dut: The DUT for which the list of test beds will be populated.
316 :param ttype: The test type for which the list of test beds will be
318 :param cadence: The cadence for which the list of test beds will be
323 :returns: Alphabeticaly sorted list of test beds.
326 return sorted(list(self.df_job_info.loc[(
327 (self.df_job_info["dut"] == dut) &
328 (self.df_job_info["ttype"] == ttype) &
329 (self.df_job_info["cadence"] == cadence)
330 )]["tbed"].unique()))
332 def _get_job(self, dut, ttype, cadence, testbed):
333 """Get the name of a job defined by dut, ttype, cadence, test bed.
334 Input information comes from the control panel.
336 :param dut: The DUT for which the job name will be created.
337 :param ttype: The test type for which the job name will be created.
338 :param cadence: The cadence for which the job name will be created.
339 :param testbed: The test bed for which the job name will be created.
347 return self.df_job_info.loc[(
348 (self.df_job_info["dut"] == dut) &
349 (self.df_job_info["ttype"] == ttype) &
350 (self.df_job_info["cadence"] == cadence) &
351 (self.df_job_info["tbed"] == testbed)
355 def _generate_options(opts: list) -> list:
356 """Return list of options for radio items in control panel. The items in
357 the list are dictionaries with keys "label" and "value".
359 :params opts: List of options (str) to be used for the generated list.
361 :returns: List of options (dict).
364 return [{"label": i, "value": i} for i in opts]
366 def _set_job_params(self, job: str) -> dict:
367 """Create a dictionary with all options and values for (and from) the
370 :params job: The name of job for and from which the dictionary will be
373 :returns: Dictionary with all options and values for (and from) the
378 lst_job = job.split("-")
383 "cadence": lst_job[4],
384 "tbed": "-".join(lst_job[-2:]),
385 "duts": self._generate_options(self._get_duts()),
386 "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
387 "cadences": self._generate_options(self._get_cadences(
388 lst_job[1], lst_job[3])),
389 "tbeds": self._generate_options(self._get_test_beds(
390 lst_job[1], lst_job[3], lst_job[4]))
393 def add_content(self):
394 """Top level method which generated the web page.
397 - Store for user input data,
399 - Main area with control panel and ploting area.
401 If no HTML layout is provided, an error message is displayed instead.
403 :returns: The HTML div with teh whole page.
411 dcc.Store(id="control-panel"),
412 dcc.Location(id="url", refresh=False),
424 self._add_ctrl_col(),
425 self._add_plotting_col(),
443 def _add_navbar(self):
444 """Add nav element with navigation panel. It is placed on the top.
446 :returns: Navigation bar.
447 :rtype: dbc.NavbarSimple
450 return dbc.NavbarSimple(
451 id="navbarsimple-main",
455 "Continuous Performance News",
464 brand_external_link=True,
469 def _add_ctrl_col(self) -> dbc.Col:
470 """Add column with control panel. It is placed on the left side.
472 :returns: Column with the control panel.
479 self._add_ctrl_panel(),
483 def _add_plotting_col(self) -> dbc.Col:
484 """Add column with tables. It is placed on the right side.
486 :returns: Column with tables.
491 id="col-plotting-area",
493 dbc.Row( # Failed tests
494 id="row-table-failed",
495 class_name="g-0 p-2",
496 children=self._default_tab_failed
499 class_name="g-0 p-2",
508 children=show_tooltip(
529 def _add_ctrl_panel(self) -> dbc.Row:
530 """Add control panel.
532 :returns: Control panel.
540 class_name="g-0 p-2",
547 children=show_tooltip(self._tooltips,
548 "help-dut", "Device under Test")
554 value=self.default["dut"],
555 options=self.default["duts"]
565 children=show_tooltip(self._tooltips,
566 "help-ttype", "Test Type"),
571 value=self.default["ttype"],
572 options=self.default["ttypes"]
581 children=show_tooltip(self._tooltips,
582 "help-cadence", "Cadence"),
587 value=self.default["cadence"],
588 options=self.default["cadences"]
597 children=show_tooltip(self._tooltips,
598 "help-tbed", "Test Bed"),
602 placeholder="Select a test bed...",
603 value=self.default["tbed"],
604 options=self.default["tbeds"]
614 children=self.default["job"]
627 def __init__(self, panel: dict, default: dict) -> None:
632 "ri-ttypes-options": default["ttypes"],
633 "ri-cadences-options": default["cadences"],
634 "dd-tbeds-options": default["tbeds"],
635 "ri-duts-value": default["dut"],
636 "ri-ttypes-value": default["ttype"],
637 "ri-cadences-value": default["cadence"],
638 "dd-tbeds-value": default["tbed"],
639 "al-job-children": default["job"]
641 self._panel = deepcopy(self._defaults)
643 for key in self._defaults:
644 self._panel[key] = panel[key]
646 def set(self, kwargs: dict) -> None:
647 for key, val in kwargs.items():
648 if key in self._panel:
649 self._panel[key] = val
651 raise KeyError(f"The key {key} is not defined.")
654 def defaults(self) -> dict:
655 return self._defaults
658 def panel(self) -> dict:
661 def get(self, key: str) -> any:
662 return self._panel[key]
664 def values(self) -> list:
665 return list(self._panel.values())
667 def callbacks(self, app):
670 Output("control-panel", "data"), # Store
671 Output("row-table-failed", "children"),
672 Output("input-url", "value"),
673 Output("ri-ttypes", "options"),
674 Output("ri-cadences", "options"),
675 Output("dd-tbeds", "options"),
676 Output("ri-duts", "value"),
677 Output("ri-ttypes", "value"),
678 Output("ri-cadences", "value"),
679 Output("dd-tbeds", "value"),
680 Output("al-job", "children"),
681 State("control-panel", "data"), # Store
682 Input("ri-duts", "value"),
683 Input("ri-ttypes", "value"),
684 Input("ri-cadences", "value"),
685 Input("dd-tbeds", "value"),
688 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
689 tbed: str, href: str) -> tuple:
693 ctrl_panel = self.ControlPanel(cp_data, self.default)
696 parsed_url = url_decode(href)
698 url_params = parsed_url["params"]
702 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
703 if trigger_id == "ri-duts":
704 ttype_opts = self._generate_options(self._get_ttypes(dut))
705 ttype_val = ttype_opts[0]["value"]
706 cad_opts = self._generate_options(
707 self._get_cadences(dut, ttype_val))
708 cad_val = cad_opts[0]["value"]
709 tbed_opts = self._generate_options(
710 self._get_test_beds(dut, ttype_val, cad_val))
711 tbed_val = tbed_opts[0]["value"]
713 "ri-duts-value": dut,
714 "ri-ttypes-options": ttype_opts,
715 "ri-ttypes-value": ttype_val,
716 "ri-cadences-options": cad_opts,
717 "ri-cadences-value": cad_val,
718 "dd-tbeds-options": tbed_opts,
719 "dd-tbeds-value": tbed_val
721 elif trigger_id == "ri-ttypes":
722 cad_opts = self._generate_options(
723 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
724 cad_val = cad_opts[0]["value"]
725 tbed_opts = self._generate_options(
726 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
728 tbed_val = tbed_opts[0]["value"]
730 "ri-ttypes-value": ttype,
731 "ri-cadences-options": cad_opts,
732 "ri-cadences-value": cad_val,
733 "dd-tbeds-options": tbed_opts,
734 "dd-tbeds-value": tbed_val
736 elif trigger_id == "ri-cadences":
737 tbed_opts = self._generate_options(
738 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
739 ctrl_panel.get("ri-ttypes-value"), cadence))
740 tbed_val = tbed_opts[0]["value"]
742 "ri-cadences-value": cadence,
743 "dd-tbeds-options": tbed_opts,
744 "dd-tbeds-value": tbed_val
746 elif trigger_id == "dd-tbeds":
748 "dd-tbeds-value": tbed
750 elif trigger_id == "url":
751 # TODO: Add verification
753 new_job = url_params.get("job", list())[0]
755 job_params = self._set_job_params(new_job)
756 ctrl_panel = self.ControlPanel(None, job_params)
758 ctrl_panel = self.ControlPanel(cp_data, self.default)
760 ctrl_panel.get("ri-duts-value"),
761 ctrl_panel.get("ri-ttypes-value"),
762 ctrl_panel.get("ri-cadences-value"),
763 ctrl_panel.get("dd-tbeds-value")
767 ctrl_panel.get("ri-duts-value"),
768 ctrl_panel.get("ri-ttypes-value"),
769 ctrl_panel.get("ri-cadences-value"),
770 ctrl_panel.get("dd-tbeds-value")
772 ctrl_panel.set({"al-job-children": job})
773 tab_failed = table_news(self.data, job)
778 gen_new_url(parsed_url, {"job": job})
780 ret_val.extend(ctrl_panel.values())