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 get_ttypes, get_cadences, get_test_beds, get_job, generate_options, \
34 from ..utils.url_processing import url_decode
35 from ..data.data import Data
36 from .tables import table_news
40 """The layout of the dash app and the callbacks.
43 def __init__(self, app: Flask, html_layout_file: str, data_spec_file: str,
44 tooltip_file: str) -> None:
46 - save the input parameters,
47 - read and pre-process the data,
48 - prepare data for the control panel,
49 - read HTML layout file,
50 - read tooltips from the tooltip file.
52 :param app: Flask application running the dash application.
53 :param html_layout_file: Path and name of the file specifying the HTML
54 layout of the dash application.
55 :param data_spec_file: Path and name of the file specifying the data to
56 be read from parquets for this application.
57 :param tooltip_file: Path and name of the yaml file specifying the
60 :type html_layout_file: str
61 :type data_spec_file: str
62 :type tooltip_file: str
67 self._html_layout_file = html_layout_file
68 self._data_spec_file = data_spec_file
69 self._tooltip_file = tooltip_file
72 data_stats, data_mrr, data_ndrpdr = Data(
73 data_spec_file=self._data_spec_file,
75 ).read_stats(days=C.NEWS_TIME_PERIOD)
77 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
79 # Prepare information for the control panel:
80 jobs = sorted(list(df_tst_info["job"].unique()))
89 lst_job = job.split("-")
90 d_job_info["job"].append(job)
91 d_job_info["dut"].append(lst_job[1])
92 d_job_info["ttype"].append(lst_job[3])
93 d_job_info["cadence"].append(lst_job[4])
94 d_job_info["tbed"].append("-".join(lst_job[-2:]))
95 self.job_info = pd.DataFrame.from_dict(d_job_info)
97 self._default = set_job_params(self.job_info, C.NEWS_DEFAULT_JOB)
99 # Pre-process the data:
101 def _create_test_name(test: str) -> str:
102 lst_tst = test.split(".")
103 suite = lst_tst[-2].replace("2n1l-", "").replace("1n1l-", "").\
105 return f"{suite.split('-')[0]}-{lst_tst[-1]}"
107 def _get_rindex(array: list, itm: any) -> int:
108 return len(array) - 1 - array[::-1].index(itm)
115 "dut_version": list(),
118 "regressions": list(),
119 "progressions": list()
122 # Create lists of failed tests:
123 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
124 last_build = max(df_job["build"].unique())
125 df_build = df_job.loc[(df_job["build"] == last_build)]
126 tst_info["job"].append(job)
127 tst_info["build"].append(last_build)
128 tst_info["start"].append(data_stats.loc[
129 (data_stats["job"] == job) &
130 (data_stats["build"] == last_build)
131 ]["start_time"].iloc[-1].strftime('%Y-%m-%d %H:%M'))
132 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
133 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
134 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
135 failed_tests = df_build.loc[(df_build["passed"] == False)]\
136 ["test_id"].to_list()
139 for tst in failed_tests:
140 l_failed.append(_create_test_name(tst))
143 tst_info["failed"].append(sorted(l_failed))
145 # Create lists of regressions and progressions:
149 tests = df_job["test_id"].unique()
151 tst_data = df_job.loc[df_job["test_id"] == test].sort_values(
152 by="start_time", ignore_index=True)
153 x_axis = tst_data["start_time"].tolist()
154 if "-ndrpdr" in test:
155 tst_data = tst_data.dropna(
156 subset=["result_pdr_lower_rate_value", ]
161 anomalies, _, _ = classify_anomalies({
162 k: v for k, v in zip(
164 tst_data["result_ndr_lower_rate_value"].tolist()
169 if "progression" in anomalies:
171 _create_test_name(test).replace("-ndrpdr", "-ndr"),
172 x_axis[_get_rindex(anomalies, "progression")]
174 if "regression" in anomalies:
176 _create_test_name(test).replace("-ndrpdr", "-ndr"),
177 x_axis[_get_rindex(anomalies, "regression")]
180 anomalies, _, _ = classify_anomalies({
181 k: v for k, v in zip(
183 tst_data["result_pdr_lower_rate_value"].tolist()
188 if "progression" in anomalies:
190 _create_test_name(test).replace("-ndrpdr", "-pdr"),
191 x_axis[_get_rindex(anomalies, "progression")]
193 if "regression" in anomalies:
195 _create_test_name(test).replace("-ndrpdr", "-pdr"),
196 x_axis[_get_rindex(anomalies, "regression")]
199 tst_data = tst_data.dropna(
200 subset=["result_receive_rate_rate_avg", ]
205 anomalies, _, _ = classify_anomalies({
206 k: v for k, v in zip(
208 tst_data["result_receive_rate_rate_avg"].\
214 if "progression" in anomalies:
216 _create_test_name(test),
217 x_axis[_get_rindex(anomalies, "progression")]
219 if "regression" in anomalies:
221 _create_test_name(test),
222 x_axis[_get_rindex(anomalies, "regression")]
225 tst_info["regressions"].append(
226 sorted(l_reg, key=lambda k: k[1], reverse=True))
227 tst_info["progressions"].append(
228 sorted(l_prog, key=lambda k: k[1], reverse=True))
230 self._data = pd.DataFrame.from_dict(tst_info)
233 self._html_layout = str()
234 self._tooltips = dict()
237 with open(self._html_layout_file, "r") as file_read:
238 self._html_layout = file_read.read()
239 except IOError as err:
241 f"Not possible to open the file {self._html_layout_file}\n{err}"
245 with open(self._tooltip_file, "r") as file_read:
246 self._tooltips = load(file_read, Loader=FullLoader)
247 except IOError as err:
249 f"Not possible to open the file {self._tooltip_file}\n{err}"
251 except YAMLError as err:
253 f"An error occurred while parsing the specification file "
254 f"{self._tooltip_file}\n{err}"
257 self._default_tab_failed = table_news(self.data, self._default["job"])
260 if self._app is not None and hasattr(self, 'callbacks'):
261 self.callbacks(self._app)
264 def html_layout(self) -> dict:
265 return self._html_layout
268 def data(self) -> pd.DataFrame:
272 def default(self) -> dict:
275 def add_content(self):
276 """Top level method which generated the web page.
279 - Store for user input data,
281 - Main area with control panel and ploting area.
283 If no HTML layout is provided, an error message is displayed instead.
285 :returns: The HTML div with the whole page.
293 dcc.Store(id="control-panel"),
294 dcc.Location(id="url", refresh=False),
306 self._add_ctrl_col(),
307 self._add_plotting_col(),
325 def _add_navbar(self):
326 """Add nav element with navigation panel. It is placed on the top.
328 :returns: Navigation bar.
329 :rtype: dbc.NavbarSimple
332 return dbc.NavbarSimple(
333 id="navbarsimple-main",
337 "Continuous Performance News",
346 brand_external_link=True,
351 def _add_ctrl_col(self) -> dbc.Col:
352 """Add column with control panel. It is placed on the left side.
354 :returns: Column with the control panel.
361 self._add_ctrl_panel(),
365 def _add_plotting_col(self) -> dbc.Col:
366 """Add column with tables. It is placed on the right side.
368 :returns: Column with tables.
373 id="col-plotting-area",
375 dbc.Row( # Failed tests
376 id="row-table-failed",
377 class_name="g-0 p-2",
378 children=self._default_tab_failed
381 class_name="g-0 p-2",
390 children=show_tooltip(
411 def _add_ctrl_panel(self) -> dbc.Row:
412 """Add control panel.
414 :returns: Control panel.
422 class_name="g-0 p-2",
429 children=show_tooltip(self._tooltips,
430 "help-dut", "Device under Test")
436 value=self.default["dut"],
437 options=self.default["duts"]
447 children=show_tooltip(self._tooltips,
448 "help-ttype", "Test Type"),
453 value=self.default["ttype"],
454 options=self.default["ttypes"]
463 children=show_tooltip(self._tooltips,
464 "help-cadence", "Cadence"),
469 value=self.default["cadence"],
470 options=self.default["cadences"]
479 children=show_tooltip(self._tooltips,
480 "help-tbed", "Test Bed"),
484 placeholder="Select a test bed...",
485 value=self.default["tbed"],
486 options=self.default["tbeds"]
496 children=self.default["job"]
506 """A class representing the control panel.
509 def __init__(self, panel: dict, default: dict) -> None:
510 """Initialisation of the control pannel by default values. If
511 particular values are provided (parameter "panel") they are set
514 :param panel: Custom values to be set to the control panel.
515 :param default: Default values to be set to the control panel.
521 "ri-ttypes-options": default["ttypes"],
522 "ri-cadences-options": default["cadences"],
523 "dd-tbeds-options": default["tbeds"],
524 "ri-duts-value": default["dut"],
525 "ri-ttypes-value": default["ttype"],
526 "ri-cadences-value": default["cadence"],
527 "dd-tbeds-value": default["tbed"],
528 "al-job-children": default["job"]
530 self._panel = deepcopy(self._defaults)
532 for key in self._defaults:
533 self._panel[key] = panel[key]
535 def set(self, kwargs: dict) -> None:
536 """Set the values of the Control panel.
538 :param kwargs: key - value pairs to be set.
540 :raises KeyError: If the key in kwargs is not present in the Control
543 for key, val in kwargs.items():
544 if key in self._panel:
545 self._panel[key] = val
547 raise KeyError(f"The key {key} is not defined.")
550 def defaults(self) -> dict:
551 return self._defaults
554 def panel(self) -> dict:
557 def get(self, key: str) -> any:
558 """Returns the value of a key from the Control panel.
560 :param key: The key which value should be returned.
562 :returns: The value of the key.
564 :raises KeyError: If the key in kwargs is not present in the Control
567 return self._panel[key]
569 def values(self) -> list:
570 """Returns the values from the Control panel as a list.
572 :returns: The values from the Control panel.
575 return list(self._panel.values())
577 def callbacks(self, app):
578 """Callbacks for the whole application.
580 :param app: The application.
585 Output("control-panel", "data"), # Store
586 Output("row-table-failed", "children"),
587 Output("input-url", "value"),
588 Output("ri-ttypes", "options"),
589 Output("ri-cadences", "options"),
590 Output("dd-tbeds", "options"),
591 Output("ri-duts", "value"),
592 Output("ri-ttypes", "value"),
593 Output("ri-cadences", "value"),
594 Output("dd-tbeds", "value"),
595 Output("al-job", "children"),
596 State("control-panel", "data"), # Store
597 Input("ri-duts", "value"),
598 Input("ri-ttypes", "value"),
599 Input("ri-cadences", "value"),
600 Input("dd-tbeds", "value"),
603 def _update_application(cp_data: dict, dut: str, ttype: str,
604 cadence:str, tbed: str, href: str) -> tuple:
605 """Update the application when the event is detected.
607 :param cp_data: Current status of the control panel stored in
609 :param dut: Input - DUT name.
610 :param ttype: Input - Test type.
611 :param cadence: Input - The cadence of the job.
612 :param tbed: Input - The test bed.
613 :param href: Input - The URL provided by the browser.
620 :returns: New values for web page elements.
624 ctrl_panel = self.ControlPanel(cp_data, self.default)
627 parsed_url = url_decode(href)
629 url_params = parsed_url["params"]
633 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
634 if trigger_id == "ri-duts":
635 ttype_opts = generate_options(get_ttypes(self.job_info, dut))
636 ttype_val = ttype_opts[0]["value"]
637 cad_opts = generate_options(
638 get_cadences(self.job_info, dut, ttype_val))
639 cad_val = cad_opts[0]["value"]
640 tbed_opts = generate_options(get_test_beds(
641 self.job_info, dut, ttype_val, cad_val))
642 tbed_val = tbed_opts[0]["value"]
644 "ri-duts-value": dut,
645 "ri-ttypes-options": ttype_opts,
646 "ri-ttypes-value": ttype_val,
647 "ri-cadences-options": cad_opts,
648 "ri-cadences-value": cad_val,
649 "dd-tbeds-options": tbed_opts,
650 "dd-tbeds-value": tbed_val
652 elif trigger_id == "ri-ttypes":
653 cad_opts = generate_options(get_cadences(
654 self.job_info, ctrl_panel.get("ri-duts-value"), ttype))
655 cad_val = cad_opts[0]["value"]
656 tbed_opts = generate_options(get_test_beds(
657 self.job_info, ctrl_panel.get("ri-duts-value"),
659 tbed_val = tbed_opts[0]["value"]
661 "ri-ttypes-value": ttype,
662 "ri-cadences-options": cad_opts,
663 "ri-cadences-value": cad_val,
664 "dd-tbeds-options": tbed_opts,
665 "dd-tbeds-value": tbed_val
667 elif trigger_id == "ri-cadences":
668 tbed_opts = generate_options(get_test_beds(
669 self.job_info, ctrl_panel.get("ri-duts-value"),
670 ctrl_panel.get("ri-ttypes-value"), cadence))
671 tbed_val = tbed_opts[0]["value"]
673 "ri-cadences-value": cadence,
674 "dd-tbeds-options": tbed_opts,
675 "dd-tbeds-value": tbed_val
677 elif trigger_id == "dd-tbeds":
679 "dd-tbeds-value": tbed
681 elif trigger_id == "url":
682 # TODO: Add verification
684 new_job = url_params.get("job", list())[0]
686 job_params = set_job_params(self.job_info, new_job)
687 ctrl_panel = self.ControlPanel(None, job_params)
689 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")
698 ctrl_panel.set({"al-job-children": job})
699 tab_failed = table_news(self.data, job)
704 gen_new_url(parsed_url, {"job": job})
706 ret_val.extend(ctrl_panel.values())