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.
18 import dash_bootstrap_components as dbc
20 from flask import Flask
23 from dash import callback_context, no_update
24 from dash import Input, Output, State
25 from dash.exceptions import PreventUpdate
26 from yaml import load, FullLoader, YAMLError
27 from datetime import datetime, timedelta
28 from copy import deepcopy
30 from ..data.data import Data
31 from .graphs import graph_statistics
38 DEFAULT_JOB = "csit-vpp-perf-mrr-daily-master-2n-icx"
40 def __init__(self, app: Flask, html_layout_file: str, spec_file: str,
41 graph_layout_file: str, data_spec_file: str,
42 time_period: int=None) -> None:
48 self._html_layout_file = html_layout_file
49 self._spec_file = spec_file
50 self._graph_layout_file = graph_layout_file
51 self._data_spec_file = data_spec_file
52 self._time_period = time_period
55 data_stats, data_mrr, data_ndrpdr = Data(
56 data_spec_file=self._data_spec_file,
58 ).read_stats(days=self._time_period)
60 df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
62 # Pre-process the data:
63 data_stats = data_stats[~data_stats.job.str.contains("-verify-")]
64 data_stats = data_stats[~data_stats.job.str.contains("-coverage-")]
65 data_stats = data_stats[~data_stats.job.str.contains("-iterative-")]
66 data_stats = data_stats[["job", "build", "start_time", "duration"]]
69 (datetime.utcnow() - data_stats["start_time"].min()).days
70 if self._time_period > data_time_period:
71 self._time_period = data_time_period
73 self._jobs = sorted(list(data_stats["job"].unique()))
81 for job in self._jobs:
82 lst_job = job.split("-")
83 job_info["job"].append(job)
84 job_info["dut"].append(lst_job[1])
85 job_info["ttype"].append(lst_job[3])
86 job_info["cadence"].append(lst_job[4])
87 job_info["tbed"].append("-".join(lst_job[-2:]))
88 self.df_job_info = pd.DataFrame.from_dict(job_info)
90 lst_job = self.DEFAULT_JOB.split("-")
92 "job": self.DEFAULT_JOB,
95 "cadence": lst_job[4],
96 "tbed": "-".join(lst_job[-2:]),
97 "duts": self._generate_options(self._get_duts()),
98 "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
99 "cadences": self._generate_options(self._get_cadences(
100 lst_job[1], lst_job[3])),
101 "tbeds": self._generate_options(self._get_test_beds(
102 lst_job[1], lst_job[3], lst_job[4]))
109 "dut_version": list(),
114 for job in self._jobs:
115 df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
116 builds = df_job["build"].unique()
118 df_build = df_job.loc[(df_job["build"] == build)]
119 tst_info["job"].append(job)
120 tst_info["build"].append(build)
121 tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
122 tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
123 tst_info["hosts"].append(df_build["hosts"].iloc[-1])
125 passed = df_build.value_counts(subset='passed')[True]
129 failed = df_build.value_counts(subset='passed')[False]
132 tst_info["passed"].append(passed)
133 tst_info["failed"].append(failed)
135 self._data = data_stats.merge(pd.DataFrame.from_dict(tst_info))
138 self._html_layout = ""
139 self._graph_layout = None
142 with open(self._html_layout_file, "r") as file_read:
143 self._html_layout = file_read.read()
144 except IOError as err:
146 f"Not possible to open the file {self._html_layout_file}\n{err}"
150 with open(self._graph_layout_file, "r") as file_read:
151 self._graph_layout = load(file_read, Loader=FullLoader)
152 except IOError as err:
154 f"Not possible to open the file {self._graph_layout_file}\n"
157 except YAMLError as err:
159 f"An error occurred while parsing the specification file "
160 f"{self._graph_layout_file}\n"
164 self._default_fig_passed, self._default_fig_duration = graph_statistics(
165 self.data, self._default["job"], self.layout
169 if self._app is not None and hasattr(self, 'callbacks'):
170 self.callbacks(self._app)
173 def html_layout(self) -> dict:
174 return self._html_layout
177 def data(self) -> pd.DataFrame:
181 def layout(self) -> dict:
182 return self._graph_layout
185 def time_period(self) -> int:
186 return self._time_period
189 def default(self) -> any:
192 def _get_duts(self) -> list:
195 return sorted(list(self.df_job_info["dut"].unique()))
197 def _get_ttypes(self, dut: str) -> list:
200 return sorted(list(self.df_job_info.loc[(
201 self.df_job_info["dut"] == dut
202 )]["ttype"].unique()))
204 def _get_cadences(self, dut: str, ttype: str) -> list:
207 return sorted(list(self.df_job_info.loc[(
208 (self.df_job_info["dut"] == dut) &
209 (self.df_job_info["ttype"] == ttype)
210 )]["cadence"].unique()))
212 def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list:
215 return sorted(list(self.df_job_info.loc[(
216 (self.df_job_info["dut"] == dut) &
217 (self.df_job_info["ttype"] == ttype) &
218 (self.df_job_info["cadence"] == cadence)
219 )]["tbed"].unique()))
221 def _get_job(self, dut, ttype, cadence, testbed):
222 """Get the name of a job defined by dut, ttype, cadence, testbed.
224 Input information comes from control panel.
226 return self.df_job_info.loc[(
227 (self.df_job_info["dut"] == dut) &
228 (self.df_job_info["ttype"] == ttype) &
229 (self.df_job_info["cadence"] == cadence) &
230 (self.df_job_info["tbed"] == testbed)
234 def add_content(self):
254 id="offcanvas-metadata",
255 title="Detailed Information",
259 dbc.Row(id="row-metadata")
267 self._add_ctrl_col(),
268 self._add_plotting_col(),
286 def _add_navbar(self):
287 """Add nav element with navigation panel. It is placed on the top.
289 return dbc.NavbarSimple(
290 id="navbarsimple-main",
294 "Continuous Performance Statistics",
303 brand_external_link=True,
308 def _add_ctrl_col(self) -> dbc.Col:
309 """Add column with controls. It is placed on the left side.
314 self._add_ctrl_panel(),
318 def _add_plotting_col(self) -> dbc.Col:
319 """Add column with plots and tables. It is placed on the right side.
322 id="col-plotting-area",
324 dbc.Row( # Passed / failed tests
325 id="row-graph-passed",
326 class_name="g-0 p-2",
328 dcc.Loading(children=[
331 figure=self._default_fig_passed
337 id="row-graph-duration",
338 class_name="g-0 p-2",
340 dcc.Loading(children=[
343 figure=self._default_fig_duration
349 id="row-btn-download",
350 class_name="g-0 p-2",
352 dcc.Loading(children=[
354 id="btn-download-data",
355 children=["Download Data"],
359 dcc.Download(id="download-data")
367 def _add_ctrl_panel(self) -> dbc.Row:
375 class_name="g-0 p-2",
387 value=self.default["dut"],
388 options=self.default["duts"]
402 value=self.default["ttype"],
403 options=self.default["ttypes"]
417 value=self.default["cadence"],
418 options=self.default["cadences"]
431 placeholder="Select a test bed...",
432 value=self.default["tbed"],
433 options=self.default["tbeds"]
443 children=self.default["job"]
450 class_name="g-0 p-2",
452 dbc.Label("Choose the Time Period"),
455 className="d-flex justify-content-center",
457 datetime.utcnow() - timedelta(
458 days=self.time_period),
459 max_date_allowed=datetime.utcnow(),
460 initial_visible_month=datetime.utcnow(),
462 datetime.utcnow() - timedelta(
463 days=self.time_period),
464 end_date=datetime.utcnow(),
465 display_format="D MMMM YY"
473 def __init__(self, panel: dict, default: dict) -> None:
475 "ri-ttypes-options": default["ttypes"],
476 "ri-cadences-options": default["cadences"],
477 "dd-tbeds-options": default["tbeds"],
478 "ri-duts-value": default["dut"],
479 "ri-ttypes-value": default["ttype"],
480 "ri-cadences-value": default["cadence"],
481 "dd-tbeds-value": default["tbed"],
482 "al-job-children": default["job"]
484 self._panel = deepcopy(self._defaults)
486 for key in self._defaults:
487 self._panel[key] = panel[key]
489 def set(self, kwargs: dict) -> None:
490 for key, val in kwargs.items():
491 if key in self._panel:
492 self._panel[key] = val
494 raise KeyError(f"The key {key} is not defined.")
497 def defaults(self) -> dict:
498 return self._defaults
501 def panel(self) -> dict:
504 def get(self, key: str) -> any:
505 return self._panel[key]
507 def values(self) -> list:
508 return list(self._panel.values())
511 def _generate_options(opts: list) -> list:
514 return [{"label": i, "value": i} for i in opts]
516 def callbacks(self, app):
519 Output("control-panel", "data"), # Store
520 Output("graph-passed", "figure"),
521 Output("graph-duration", "figure"),
522 Output("ri-ttypes", "options"),
523 Output("ri-cadences", "options"),
524 Output("dd-tbeds", "options"),
525 Output("ri-duts", "value"),
526 Output("ri-ttypes", "value"),
527 Output("ri-cadences", "value"),
528 Output("dd-tbeds", "value"),
529 Output("al-job", "children"),
530 State("control-panel", "data"), # Store
531 Input("ri-duts", "value"),
532 Input("ri-ttypes", "value"),
533 Input("ri-cadences", "value"),
534 Input("dd-tbeds", "value"),
535 Input("dpr-period", "start_date"),
536 Input("dpr-period", "end_date"),
537 prevent_initial_call=True
539 def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
540 tbed: str, d_start: str, d_end: str) -> tuple:
544 ctrl_panel = self.ControlPanel(cp_data, self.default)
546 d_start = datetime(int(d_start[0:4]), int(d_start[5:7]),
548 d_end = datetime(int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10]))
550 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
551 if trigger_id == "ri-duts":
552 ttype_opts = self._generate_options(self._get_ttypes(dut))
553 ttype_val = ttype_opts[0]["value"]
554 cad_opts = self._generate_options(
555 self._get_cadences(dut, ttype_val))
556 cad_val = cad_opts[0]["value"]
557 tbed_opts = self._generate_options(
558 self._get_test_beds(dut, ttype_val, cad_val))
559 tbed_val = tbed_opts[0]["value"]
561 "ri-duts-value": dut,
562 "ri-ttypes-options": ttype_opts,
563 "ri-ttypes-value": ttype_val,
564 "ri-cadences-options": cad_opts,
565 "ri-cadences-value": cad_val,
566 "dd-tbeds-options": tbed_opts,
567 "dd-tbeds-value": tbed_val
569 elif trigger_id == "ri-ttypes":
570 cad_opts = self._generate_options(
571 self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
572 cad_val = cad_opts[0]["value"]
573 tbed_opts = self._generate_options(
574 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
576 tbed_val = tbed_opts[0]["value"]
578 "ri-ttypes-value": ttype,
579 "ri-cadences-options": cad_opts,
580 "ri-cadences-value": cad_val,
581 "dd-tbeds-options": tbed_opts,
582 "dd-tbeds-value": tbed_val
584 elif trigger_id == "ri-cadences":
585 tbed_opts = self._generate_options(
586 self._get_test_beds(ctrl_panel.get("ri-duts-value"),
587 ctrl_panel.get("ri-ttypes-value"), cadence))
588 tbed_val = tbed_opts[0]["value"]
590 "ri-cadences-value": cadence,
591 "dd-tbeds-options": tbed_opts,
592 "dd-tbeds-value": tbed_val
594 elif trigger_id == "dd-tbeds":
596 "dd-tbeds-value": tbed
598 elif trigger_id == "dpr-period":
602 ctrl_panel.get("ri-duts-value"),
603 ctrl_panel.get("ri-ttypes-value"),
604 ctrl_panel.get("ri-cadences-value"),
605 ctrl_panel.get("dd-tbeds-value")
607 ctrl_panel.set({"al-job-children": job})
608 fig_passed, fig_duration = graph_statistics(
609 self.data, job, self.layout, d_start, d_end)
611 ret_val = [ctrl_panel.panel, fig_passed, fig_duration]
612 ret_val.extend(ctrl_panel.values())
616 Output("download-data", "data"),
617 Input("btn-download-data", "n_clicks"),
618 prevent_initial_call=True
620 def _download_data(n_clicks):
626 return dcc.send_data_frame(self.data.to_csv, "statistics.csv")
629 Output("row-metadata", "children"),
630 Output("offcanvas-metadata", "is_open"),
631 Input("graph-passed", "clickData"),
632 Input("graph-duration", "clickData"),
633 prevent_initial_call=True
635 def _show_metadata_from_graphs(
636 passed_data: dict, duration_data: dict) -> tuple:
640 if not (passed_data or duration_data):
645 title = "Job Statistics"
646 trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
647 if trigger_id == "graph-passed":
648 graph_data = passed_data["points"][0].get("hovertext", "")
649 elif trigger_id == "graph-duration":
650 graph_data = duration_data["points"][0].get("text", "")
654 class_name="gy-2 p-0",
656 dbc.CardHeader(children=[
658 target_id="metadata",
660 style={"display": "inline-block"}
667 children=[dbc.ListGroup(
676 ) for x in graph_data.split("<br>")
686 return metadata, open_canvas