1 # Copyright (c) 2024 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.
18 import dash_bootstrap_components as dbc
20 from flask import Flask
23 from dash import callback_context
24 from dash import Input, Output, State
26 from ..utils.constants import Constants as C
27 from ..utils.utils import gen_new_url, navbar_trending
28 from ..utils.anomalies import classify_anomalies
29 from ..utils.url_processing import url_decode
30 from .tables import table_summary
34 """The layout of the dash app and the callbacks.
40 data_stats: pd.DataFrame,
41 data_trending: pd.DataFrame,
45 - save the input parameters,
46 - read and pre-process the data,
47 - prepare data for the control panel,
48 - read HTML layout file,
49 - read tooltips from the tooltip file.
51 :param app: Flask application running the dash application.
52 :param data_stats: Pandas dataframe with staistical data.
53 :param data_trending: Pandas dataframe with trending data.
54 :param html_layout_file: Path and name of the file specifying the HTML
55 layout of the dash application.
57 :type data_stats: pandas.DataFrame
58 :type data_trending: pandas.DataFrame
59 :type html_layout_file: str
64 self._html_layout_file = html_layout_file
66 # Prepare information for the control panel:
67 self._jobs = sorted(list(data_trending["job"].unique()))
75 for job in self._jobs:
76 lst_job = job.split("-")
77 d_job_info["job"].append(job)
78 d_job_info["dut"].append(lst_job[1])
79 d_job_info["ttype"].append(lst_job[3])
80 d_job_info["cadence"].append(lst_job[4])
81 d_job_info["tbed"].append("-".join(lst_job[-2:]))
82 self.job_info = pd.DataFrame.from_dict(d_job_info)
84 # Pre-process the data:
86 def _create_test_name(test: str) -> str:
87 lst_tst = test.split(".")
88 suite = lst_tst[-2].replace("2n1l-", "").replace("1n1l-", "").\
90 return f"{suite.split('-')[0]}-{lst_tst[-1]}"
92 def _get_rindex(array: list, itm: any) -> int:
93 return len(array) - 1 - array[::-1].index(itm)
100 "dut_version": list(),
103 "regressions": list(),
104 "progressions": list()
106 for job in self._jobs:
107 # Create lists of failed tests:
108 df_job = data_trending.loc[(data_trending["job"] == job)]
109 last_build = str(max(pd.to_numeric(df_job["build"].unique())))
110 df_build = df_job.loc[(df_job["build"] == last_build)]
111 tst_info["job"].append(job)
112 tst_info["build"].append(last_build)
113 tst_info["start"].append(data_stats.loc[
114 (data_stats["job"] == job) &
115 (data_stats["build"] == last_build)
116 ]["start_time"].iloc[-1].strftime('%Y-%m-%d %H:%M'))
117 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
118 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
119 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
120 failed_tests = df_build.loc[(df_build["passed"] == False)]\
121 ["test_id"].to_list()
124 for tst in failed_tests:
125 l_failed.append(_create_test_name(tst))
128 tst_info["failed"].append(sorted(l_failed))
130 # Create lists of regressions and progressions:
134 tests = df_job["test_id"].unique()
136 tst_data = df_job.loc[(
137 (df_job["test_id"] == test) &
138 (df_job["passed"] == True)
139 )].sort_values(by="start_time", ignore_index=True)
140 if "-ndrpdr" in test:
141 tst_data = tst_data.dropna(
142 subset=["result_pdr_lower_rate_value", ]
146 x_axis = tst_data["start_time"].tolist()
148 anomalies, _, _ = classify_anomalies({
149 k: v for k, v in zip(
151 tst_data["result_ndr_lower_rate_value"].tolist()
156 if "progression" in anomalies:
158 _create_test_name(test).replace("-ndrpdr", "-ndr"),
159 x_axis[_get_rindex(anomalies, "progression")]
161 if "regression" in anomalies:
163 _create_test_name(test).replace("-ndrpdr", "-ndr"),
164 x_axis[_get_rindex(anomalies, "regression")]
167 anomalies, _, _ = classify_anomalies({
168 k: v for k, v in zip(
170 tst_data["result_pdr_lower_rate_value"].tolist()
175 if "progression" in anomalies:
177 _create_test_name(test).replace("-ndrpdr", "-pdr"),
178 x_axis[_get_rindex(anomalies, "progression")]
180 if "regression" in anomalies:
182 _create_test_name(test).replace("-ndrpdr", "-pdr"),
183 x_axis[_get_rindex(anomalies, "regression")]
186 tst_data = tst_data.dropna(
187 subset=["result_receive_rate_rate_avg", ]
191 x_axis = tst_data["start_time"].tolist()
193 anomalies, _, _ = classify_anomalies({
194 k: v for k, v in zip(
196 tst_data["result_receive_rate_rate_avg"].\
202 if "progression" in anomalies:
204 _create_test_name(test),
205 x_axis[_get_rindex(anomalies, "progression")]
207 if "regression" in anomalies:
209 _create_test_name(test),
210 x_axis[_get_rindex(anomalies, "regression")]
213 tst_info["regressions"].append(
214 sorted(l_reg, key=lambda k: k[1], reverse=True))
215 tst_info["progressions"].append(
216 sorted(l_prog, key=lambda k: k[1], reverse=True))
218 self._data = pd.DataFrame.from_dict(tst_info)
221 self._html_layout = str()
224 with open(self._html_layout_file, "r") as file_read:
225 self._html_layout = file_read.read()
226 except IOError as err:
228 f"Not possible to open the file {self._html_layout_file}\n{err}"
231 self._default_period = C.NEWS_SHORT
232 self._default_active = (False, True, False)
235 if self._app is not None and hasattr(self, 'callbacks'):
236 self.callbacks(self._app)
239 def html_layout(self) -> dict:
240 return self._html_layout
242 def add_content(self):
243 """Top level method which generated the web page.
246 - Store for user input data,
248 - Main area with control panel and ploting area.
250 If no HTML layout is provided, an error message is displayed instead.
252 :returns: The HTML div with the whole page.
261 dcc.Location(id="url", refresh=False),
265 children=[navbar_trending((False, True, False, False))]
271 self._add_ctrl_col(),
272 self._add_plotting_col()
277 id="offcanvas-documentation",
278 title="Documentation",
281 children=html.Iframe(
282 src=C.URL_DOC_TRENDING,
302 def _add_ctrl_col(self) -> dbc.Col:
303 """Add column with control panel. It is placed on the left side.
305 :returns: Column with the control panel.
310 children=self._add_ctrl_panel(),
311 className="sticky-top"
315 def _add_plotting_col(self) -> dbc.Col:
316 """Add column with tables. It is placed on the right side.
318 :returns: Column with tables.
322 id="col-plotting-area",
328 class_name="g-0 p-0",
339 def _add_ctrl_panel(self) -> list:
340 """Add control panel.
342 :returns: Control panel.
347 class_name="g-0 p-1",
361 children=f"Last {C.NEWS_SHORT} Runs",
380 def _get_plotting_area(
385 """Generate the plotting area with all its content.
387 :param period: The time period for summary tables.
388 :param url: URL to be displayed in the modal window.
391 :returns: The content of the plotting area.
397 class_name="g-0 p-1",
398 children=table_summary(self._data, self._jobs, period)
410 "text-transform": "none",
411 "padding": "0rem 1rem"
416 dbc.ModalHeader(dbc.ModalTitle("URL")),
426 "d-grid gap-0 d-md-flex justify-content-md-end"
433 def callbacks(self, app):
434 """Callbacks for the whole application.
436 :param app: The application.
441 Output("plotting-area", "children"),
442 Output("period-last", "active"),
443 Output("period-short", "active"),
444 Output("period-long", "active"),
445 Input("url", "href"),
446 Input("period-last", "n_clicks"),
447 Input("period-short", "n_clicks"),
448 Input("period-long", "n_clicks")
450 def _update_application(href: str, *_) -> tuple:
451 """Update the application when the event is detected.
453 :returns: New values for web page elements.
458 "period-last": C.NEWS_LAST,
459 "period-short": C.NEWS_SHORT,
460 "period-long": C.NEWS_LONG
463 "period-last": (True, False, False),
464 "period-short": (False, True, False),
465 "period-long": (False, False, True)
469 parsed_url = url_decode(href)
471 url_params = parsed_url["params"]
475 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
476 if trigger_id == "url" and url_params:
477 trigger_id = url_params.get("period", list())[0]
480 self._get_plotting_area(
481 periods.get(trigger_id, self._default_period),
482 gen_new_url(parsed_url, {"period": trigger_id})
485 ret_val.extend(actives.get(trigger_id, self._default_active))
489 Output("plot-mod-url", "is_open"),
490 Input("plot-btn-url", "n_clicks"),
491 State("plot-mod-url", "is_open")
493 def toggle_plot_mod_url(n, is_open):
494 """Toggle the modal window with url.
501 Output("offcanvas-documentation", "is_open"),
502 Input("btn-documentation", "n_clicks"),
503 State("offcanvas-documentation", "is_open")
505 def toggle_offcanvas_documentation(n_clicks, is_open):