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, table_summary
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 self._jobs = sorted(list(df_tst_info["job"].unique()))
88 for job in self._jobs:
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()
121 for job in self._jobs:
122 # Create lists of failed tests:
123 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
124 last_build = str(max(pd.to_numeric(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 = \
258 table_news(self.data, self._default["job"], C.NEWS_TIME_PERIOD)
261 if self._app is not None and hasattr(self, 'callbacks'):
262 self.callbacks(self._app)
265 def html_layout(self) -> dict:
266 return self._html_layout
269 def data(self) -> pd.DataFrame:
273 def default(self) -> dict:
276 def add_content(self):
277 """Top level method which generated the web page.
280 - Store for user input data,
282 - Main area with control panel and ploting area.
284 If no HTML layout is provided, an error message is displayed instead.
286 :returns: The HTML div with the whole page.
294 dcc.Store(id="control-panel"),
295 dcc.Location(id="url", refresh=False),
307 self._add_ctrl_col(),
308 self._add_plotting_col(),
326 def _add_navbar(self):
327 """Add nav element with navigation panel. It is placed on the top.
329 :returns: Navigation bar.
330 :rtype: dbc.NavbarSimple
333 return dbc.NavbarSimple(
334 id="navbarsimple-main",
338 "Continuous Performance News",
347 brand_external_link=True,
352 def _add_ctrl_col(self) -> dbc.Col:
353 """Add column with control panel. It is placed on the left side.
355 :returns: Column with the control panel.
362 self._add_ctrl_panel(),
366 def _add_plotting_col(self) -> dbc.Col:
367 """Add column with tables. It is placed on the right side.
369 :returns: Column with tables.
374 id="col-plotting-area",
376 dbc.Row( # Failed tests
377 id="row-table-failed",
378 class_name="g-0 p-2",
379 children=self._default_tab_failed
382 class_name="g-0 p-2",
391 children=show_tooltip(
412 def _add_ctrl_panel(self) -> dbc.Row:
413 """Add control panel.
415 :returns: Control panel.
423 class_name="g-0 p-2",
430 children=show_tooltip(self._tooltips,
431 "help-dut", "Device under Test")
437 value=self.default["dut"],
438 options=self.default["duts"]
448 children=show_tooltip(self._tooltips,
449 "help-ttype", "Test Type"),
454 value=self.default["ttype"],
455 options=self.default["ttypes"]
464 children=show_tooltip(self._tooltips,
465 "help-cadence", "Cadence"),
470 value=self.default["cadence"],
471 options=self.default["cadences"]
480 children=show_tooltip(self._tooltips,
481 "help-tbed", "Test Bed"),
485 placeholder="Select a test bed...",
486 value=self.default["tbed"],
487 options=self.default["tbeds"]
492 class_name="gy-1 p-0",
499 f"Show Summary from the last "
500 f"{C.NEWS_SUMMARY_PERIOD} Days"
516 children=self.default["job"]
526 """A class representing the control panel.
529 def __init__(self, panel: dict, default: dict) -> None:
530 """Initialisation of the control pannel by default values. If
531 particular values are provided (parameter "panel") they are set
534 :param panel: Custom values to be set to the control panel.
535 :param default: Default values to be set to the control panel.
541 "ri-ttypes-options": default["ttypes"],
542 "ri-cadences-options": default["cadences"],
543 "dd-tbeds-options": default["tbeds"],
544 "ri-duts-value": default["dut"],
545 "ri-ttypes-value": default["ttype"],
546 "ri-cadences-value": default["cadence"],
547 "dd-tbeds-value": default["tbed"],
548 "al-job-children": default["job"]
550 self._panel = deepcopy(self._defaults)
552 for key in self._defaults:
553 self._panel[key] = panel[key]
555 def set(self, kwargs: dict) -> None:
556 """Set the values of the Control panel.
558 :param kwargs: key - value pairs to be set.
560 :raises KeyError: If the key in kwargs is not present in the Control
563 for key, val in kwargs.items():
564 if key in self._panel:
565 self._panel[key] = val
567 raise KeyError(f"The key {key} is not defined.")
570 def defaults(self) -> dict:
571 return self._defaults
574 def panel(self) -> dict:
577 def get(self, key: str) -> any:
578 """Returns the value of a key from the Control panel.
580 :param key: The key which value should be returned.
582 :returns: The value of the key.
584 :raises KeyError: If the key in kwargs is not present in the Control
587 return self._panel[key]
589 def values(self) -> list:
590 """Returns the values from the Control panel as a list.
592 :returns: The values from the Control panel.
595 return list(self._panel.values())
597 def callbacks(self, app):
598 """Callbacks for the whole application.
600 :param app: The application.
605 Output("control-panel", "data"), # Store
606 Output("row-table-failed", "children"),
607 Output("input-url", "value"),
608 Output("ri-ttypes", "options"),
609 Output("ri-cadences", "options"),
610 Output("dd-tbeds", "options"),
611 Output("ri-duts", "value"),
612 Output("ri-ttypes", "value"),
613 Output("ri-cadences", "value"),
614 Output("dd-tbeds", "value"),
615 Output("al-job", "children"),
616 State("control-panel", "data"), # Store
617 Input("ri-duts", "value"),
618 Input("ri-ttypes", "value"),
619 Input("ri-cadences", "value"),
620 Input("dd-tbeds", "value"),
621 Input("url", "href"),
622 Input("btn-summary", "n_clicks")
624 def _update_application(cp_data: dict, dut: str, ttype: str,
625 cadence:str, tbed: str, href: str, btn_all: int) -> tuple:
626 """Update the application when the event is detected.
628 :param cp_data: Current status of the control panel stored in
630 :param dut: Input - DUT name.
631 :param ttype: Input - Test type.
632 :param cadence: Input - The cadence of the job.
633 :param tbed: Input - The test bed.
634 :param href: Input - The URL provided by the browser.
641 :returns: New values for web page elements.
645 ctrl_panel = self.ControlPanel(cp_data, self.default)
648 parsed_url = url_decode(href)
650 url_params = parsed_url["params"]
656 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
657 if trigger_id == "ri-duts":
658 ttype_opts = generate_options(get_ttypes(self.job_info, dut))
659 ttype_val = ttype_opts[0]["value"]
660 cad_opts = generate_options(
661 get_cadences(self.job_info, dut, ttype_val))
662 cad_val = cad_opts[0]["value"]
663 tbed_opts = generate_options(get_test_beds(
664 self.job_info, dut, ttype_val, cad_val))
665 tbed_val = tbed_opts[0]["value"]
667 "ri-duts-value": dut,
668 "ri-ttypes-options": ttype_opts,
669 "ri-ttypes-value": ttype_val,
670 "ri-cadences-options": cad_opts,
671 "ri-cadences-value": cad_val,
672 "dd-tbeds-options": tbed_opts,
673 "dd-tbeds-value": tbed_val
675 elif trigger_id == "ri-ttypes":
676 cad_opts = generate_options(get_cadences(
677 self.job_info, ctrl_panel.get("ri-duts-value"), ttype))
678 cad_val = cad_opts[0]["value"]
679 tbed_opts = generate_options(get_test_beds(
680 self.job_info, ctrl_panel.get("ri-duts-value"),
682 tbed_val = tbed_opts[0]["value"]
684 "ri-ttypes-value": ttype,
685 "ri-cadences-options": cad_opts,
686 "ri-cadences-value": cad_val,
687 "dd-tbeds-options": tbed_opts,
688 "dd-tbeds-value": tbed_val
690 elif trigger_id == "ri-cadences":
691 tbed_opts = generate_options(get_test_beds(
692 self.job_info, ctrl_panel.get("ri-duts-value"),
693 ctrl_panel.get("ri-ttypes-value"), cadence))
694 tbed_val = tbed_opts[0]["value"]
696 "ri-cadences-value": cadence,
697 "dd-tbeds-options": tbed_opts,
698 "dd-tbeds-value": tbed_val
700 elif trigger_id == "dd-tbeds":
702 "dd-tbeds-value": tbed
704 elif trigger_id == "url":
705 # TODO: Add verification
707 new_job = url_params.get("job", list())[0]
708 if new_job and new_job != "all":
709 job_params = set_job_params(self.job_info, new_job)
710 ctrl_panel = self.ControlPanel(None, job_params)
711 if new_job and new_job == "all":
714 ctrl_panel = self.ControlPanel(cp_data, self.default)
715 elif trigger_id == "btn-summary":
721 f"Summary from the last {C.NEWS_SUMMARY_PERIOD} days"
724 tables = table_summary(self.data, self._jobs)
728 ctrl_panel.get("ri-duts-value"),
729 ctrl_panel.get("ri-ttypes-value"),
730 ctrl_panel.get("ri-cadences-value"),
731 ctrl_panel.get("dd-tbeds-value")
733 ctrl_panel.set({"al-job-children": job})
734 tables = table_news(self.data, job, C.NEWS_TIME_PERIOD)
739 gen_new_url(parsed_url, {"job": job})
741 ret_val.extend(ctrl_panel.values())