X-Git-Url: https://gerrit.fd.io/r/gitweb?p=csit.git;a=blobdiff_plain;f=resources%2Ftools%2Fdash%2Fapp%2Fpal%2Fstats%2Flayout.py;h=7bd2dee6200a373202d136ff89b8ff6a330406f7;hp=2e74fb8b3c298cba76b98ebe6138d9f7545af43d;hb=2f9e211bd097fdd2f83df540aa494d924df01c7d;hpb=7889544ddfba775bf07bcd331f022de4650e59dc diff --git a/resources/tools/dash/app/pal/stats/layout.py b/resources/tools/dash/app/pal/stats/layout.py index 2e74fb8b3c..7bd2dee620 100644 --- a/resources/tools/dash/app/pal/stats/layout.py +++ b/resources/tools/dash/app/pal/stats/layout.py @@ -29,20 +29,44 @@ from datetime import datetime, timedelta from copy import deepcopy from ..utils.constants import Constants as C -from ..utils.utils import show_tooltip, gen_new_url +from ..utils.utils import show_tooltip, gen_new_url, get_date, get_ttypes, \ + get_cadences, get_test_beds, get_job, generate_options, set_job_params from ..utils.url_processing import url_decode from ..data.data import Data from .graphs import graph_statistics, select_data class Layout: - """ + """The layout of the dash app and the callbacks. """ def __init__(self, app: Flask, html_layout_file: str, graph_layout_file: str, data_spec_file: str, tooltip_file: str, time_period: int=None) -> None: - """ + """Initialization: + - save the input parameters, + - read and pre-process the data, + - prepare data for the control panel, + - read HTML layout file, + - read tooltips from the tooltip file. + + :param app: Flask application running the dash application. + :param html_layout_file: Path and name of the file specifying the HTML + layout of the dash application. + :param graph_layout_file: Path and name of the file with layout of + plot.ly graphs. + :param data_spec_file: Path and name of the file specifying the data to + be read from parquets for this application. + :param tooltip_file: Path and name of the yaml file specifying the + tooltips. + :param time_period: It defines the time period for data read from the + parquets in days from now back to the past. + :type app: Flask + :type html_layout_file: str + :type graph_layout_file: str + :type data_spec_file: str + :type tooltip_file: str + :type time_period: int """ # Inputs @@ -73,7 +97,7 @@ class Layout: self._time_period = data_time_period jobs = sorted(list(data_stats["job"].unique())) - job_info = { + d_job_info = { "job": list(), "dut": list(), "ttype": list(), @@ -82,14 +106,14 @@ class Layout: } for job in jobs: lst_job = job.split("-") - job_info["job"].append(job) - job_info["dut"].append(lst_job[1]) - job_info["ttype"].append(lst_job[3]) - job_info["cadence"].append(lst_job[4]) - job_info["tbed"].append("-".join(lst_job[-2:])) - self.df_job_info = pd.DataFrame.from_dict(job_info) + d_job_info["job"].append(job) + d_job_info["dut"].append(lst_job[1]) + d_job_info["ttype"].append(lst_job[3]) + d_job_info["cadence"].append(lst_job[4]) + d_job_info["tbed"].append("-".join(lst_job[-2:])) + self.job_info = pd.DataFrame.from_dict(d_job_info) - self._default = self._set_job_params(C.STATS_DEFAULT_JOB) + self._default = set_job_params(self.job_info, C.STATS_DEFAULT_JOB) tst_info = { "job": list(), @@ -203,68 +227,20 @@ class Layout: def default(self) -> any: return self._default - def _get_duts(self) -> list: - """ - """ - return sorted(list(self.df_job_info["dut"].unique())) - - def _get_ttypes(self, dut: str) -> list: - """ - """ - return sorted(list(self.df_job_info.loc[( - self.df_job_info["dut"] == dut - )]["ttype"].unique())) - - def _get_cadences(self, dut: str, ttype: str) -> list: - """ - """ - return sorted(list(self.df_job_info.loc[( - (self.df_job_info["dut"] == dut) & - (self.df_job_info["ttype"] == ttype) - )]["cadence"].unique())) + def add_content(self): + """Top level method which generated the web page. - def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list: - """ - """ - return sorted(list(self.df_job_info.loc[( - (self.df_job_info["dut"] == dut) & - (self.df_job_info["ttype"] == ttype) & - (self.df_job_info["cadence"] == cadence) - )]["tbed"].unique())) + It generates: + - Store for user input data, + - Navigation bar, + - Main area with control panel and ploting area. - def _get_job(self, dut, ttype, cadence, testbed): - """Get the name of a job defined by dut, ttype, cadence, testbed. + If no HTML layout is provided, an error message is displayed instead. - Input information comes from control panel. - """ - return self.df_job_info.loc[( - (self.df_job_info["dut"] == dut) & - (self.df_job_info["ttype"] == ttype) & - (self.df_job_info["cadence"] == cadence) & - (self.df_job_info["tbed"] == testbed) - )]["job"].item() - - def _set_job_params(self, job: str) -> dict: - """ + :returns: The HTML div with teh whole page. + :rtype: html.Div """ - lst_job = job.split("-") - return { - "job": job, - "dut": lst_job[1], - "ttype": lst_job[3], - "cadence": lst_job[4], - "tbed": "-".join(lst_job[-2:]), - "duts": self._generate_options(self._get_duts()), - "ttypes": self._generate_options(self._get_ttypes(lst_job[1])), - "cadences": self._generate_options(self._get_cadences( - lst_job[1], lst_job[3])), - "tbeds": self._generate_options(self._get_test_beds( - lst_job[1], lst_job[3], lst_job[4])) - } - def add_content(self): - """ - """ if self.html_layout: return html.Div( id="div-main", @@ -315,6 +291,9 @@ class Layout: def _add_navbar(self): """Add nav element with navigation panel. It is placed on the top. + + :returns: Navigation bar. + :rtype: dbc.NavbarSimple """ return dbc.NavbarSimple( id="navbarsimple-main", @@ -337,6 +316,9 @@ class Layout: def _add_ctrl_col(self) -> dbc.Col: """Add column with controls. It is placed on the left side. + + :returns: Column with the control panel. + :rtype: dbc.Col """ return dbc.Col( id="col-controls", @@ -347,6 +329,9 @@ class Layout: def _add_plotting_col(self) -> dbc.Col: """Add column with plots and tables. It is placed on the right side. + + :returns: Column with tables. + :rtype: dbc.Col """ return dbc.Col( id="col-plotting-area", @@ -427,7 +412,10 @@ class Layout: ) def _add_ctrl_panel(self) -> dbc.Row: - """ + """Add control panel. + + :returns: Control panel. + :rtype: dbc.Row """ return dbc.Row( id="row-ctrl-panel", @@ -542,7 +530,20 @@ class Layout: ) class ControlPanel: + """A class representing the control panel. + """ + def __init__(self, panel: dict, default: dict) -> None: + """Initialisation of the control pannel by default values. If + particular values are provided (parameter "panel") they are set + afterwards. + + :param panel: Custom values to be set to the control panel. + :param default: Default values to be set to the control panel. + :type panel: dict + :type defaults: dict + """ + self._defaults = { "ri-ttypes-options": default["ttypes"], "ri-cadences-options": default["cadences"], @@ -559,6 +560,13 @@ class Layout: self._panel[key] = panel[key] def set(self, kwargs: dict) -> None: + """Set the values of the Control panel. + + :param kwargs: key - value pairs to be set. + :type kwargs: dict + :raises KeyError: If the key in kwargs is not present in the Control + panel. + """ for key, val in kwargs.items(): if key in self._panel: self._panel[key] = val @@ -574,20 +582,32 @@ class Layout: return self._panel def get(self, key: str) -> any: + """Returns the value of a key from the Control panel. + + :param key: The key which value should be returned. + :type key: str + :returns: The value of the key. + :rtype: any + :raises KeyError: If the key in kwargs is not present in the Control + panel. + """ return self._panel[key] def values(self) -> list: - return list(self._panel.values()) + """Returns the values from the Control panel as a list. - @staticmethod - def _generate_options(opts: list) -> list: - return [{"label": i, "value": i} for i in opts] + :returns: The values from the Control panel. + :rtype: list + """ + return list(self._panel.values()) - @staticmethod - def _get_date(s_date: str) -> datetime: - return datetime(int(s_date[0:4]), int(s_date[5:7]), int(s_date[8:10])) def callbacks(self, app): + """Callbacks for the whole application. + + :param app: The application. + :type app: Flask + """ @app.callback( Output("control-panel", "data"), # Store @@ -611,15 +631,35 @@ class Layout: Input("dpr-period", "end_date"), Input("url", "href") ) - def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str, + def _update_ctrl_panel(cp_data: dict, dut: str, ttype: str, cadence:str, tbed: str, start: str, end: str, href: str) -> tuple: - """ + """Update the application when the event is detected. + + :param cp_data: Current status of the control panel stored in + browser. + :param dut: Input - DUT name. + :param ttype: Input - Test type. + :param cadence: Input - The cadence of the job. + :param tbed: Input - The test bed. + :param start: Date and time where the data processing starts. + :param end: Date and time where the data processing ends. + :param href: Input - The URL provided by the browser. + :type cp_data: dict + :type dut: str + :type ttype: str + :type cadence: str + :type tbed: str + :type start: str + :type end: str + :type href: str + :returns: New values for web page elements. + :rtype: tuple """ ctrl_panel = self.ControlPanel(cp_data, self.default) - start = self._get_date(start) - end = self._get_date(end) + start = get_date(start) + end = get_date(end) # Parse the url: parsed_url = url_decode(href) @@ -630,13 +670,13 @@ class Layout: trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0] if trigger_id == "ri-duts": - ttype_opts = self._generate_options(self._get_ttypes(dut)) + ttype_opts = generate_options(get_ttypes(self.job_info, dut)) ttype_val = ttype_opts[0]["value"] - cad_opts = self._generate_options( - self._get_cadences(dut, ttype_val)) + cad_opts = generate_options(get_cadences( + self.job_info, dut, ttype_val)) cad_val = cad_opts[0]["value"] - tbed_opts = self._generate_options( - self._get_test_beds(dut, ttype_val, cad_val)) + tbed_opts = generate_options(get_test_beds( + self.job_info, dut, ttype_val, cad_val)) tbed_val = tbed_opts[0]["value"] ctrl_panel.set({ "ri-duts-value": dut, @@ -648,12 +688,12 @@ class Layout: "dd-tbeds-value": tbed_val }) elif trigger_id == "ri-ttypes": - cad_opts = self._generate_options( - self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype)) + cad_opts = generate_options(get_cadences( + self.job_info, ctrl_panel.get("ri-duts-value"), ttype)) cad_val = cad_opts[0]["value"] - tbed_opts = self._generate_options( - self._get_test_beds(ctrl_panel.get("ri-duts-value"), - ttype, cad_val)) + tbed_opts = generate_options(get_test_beds( + self.job_info, ctrl_panel.get("ri-duts-value"), ttype, + cad_val)) tbed_val = tbed_opts[0]["value"] ctrl_panel.set({ "ri-ttypes-value": ttype, @@ -663,8 +703,8 @@ class Layout: "dd-tbeds-value": tbed_val }) elif trigger_id == "ri-cadences": - tbed_opts = self._generate_options( - self._get_test_beds(ctrl_panel.get("ri-duts-value"), + tbed_opts = generate_options(get_test_beds( + self.job_info, ctrl_panel.get("ri-duts-value"), ctrl_panel.get("ri-ttypes-value"), cadence)) tbed_val = tbed_opts[0]["value"] ctrl_panel.set({ @@ -685,20 +725,15 @@ class Layout: new_start = url_params.get("start", list())[0] new_end = url_params.get("end", list())[0] if new_job and new_start and new_end: - start = self._get_date(new_start) - end = self._get_date(new_end) - job_params = self._set_job_params(new_job) + start = get_date(new_start) + end = get_date(new_end) + job_params = set_job_params(self.job_info, new_job) ctrl_panel = self.ControlPanel(None, job_params) else: ctrl_panel = self.ControlPanel(cp_data, self.default) - job = self._get_job( - ctrl_panel.get("ri-duts-value"), - ctrl_panel.get("ri-ttypes-value"), - ctrl_panel.get("ri-cadences-value"), - ctrl_panel.get("dd-tbeds-value") - ) - job = self._get_job( + job = get_job( + self.job_info, ctrl_panel.get("ri-duts-value"), ctrl_panel.get("ri-ttypes-value"), ctrl_panel.get("ri-cadences-value"), @@ -734,23 +769,35 @@ class Layout: prevent_initial_call=True ) def _download_data(cp_data: dict, start: str, end: str, n_clicks: int): - """ + """Download the data + + :param cp_data: Current status of the control panel stored in + browser. + :param start: Date and time where the data processing starts. + :param end: Date and time where the data processing ends. + :param n_clicks: Number of clicks on the button "Download". + :type cp_data: dict + :type start: str + :type end: str + :type n_clicks: int + :returns: dict of data frame content (base64 encoded) and meta data + used by the Download component. + :rtype: dict """ if not (n_clicks): raise PreventUpdate ctrl_panel = self.ControlPanel(cp_data, self.default) - job = self._get_job( + job = get_job( + self.job_info, ctrl_panel.get("ri-duts-value"), ctrl_panel.get("ri-ttypes-value"), ctrl_panel.get("ri-cadences-value"), ctrl_panel.get("dd-tbeds-value") ) - start = datetime(int(start[0:4]), int(start[5:7]), int(start[8:10])) - end = datetime(int(end[0:4]), int(end[5:7]), int(end[8:10])) - data = select_data(self.data, job, start, end) + data = select_data(self.data, job, get_date(start), get_date(end)) data = data.drop(columns=["job", ]) return dcc.send_data_frame(data.T.to_csv, f"{job}-stats.csv") @@ -764,7 +811,19 @@ class Layout: ) def _show_metadata_from_graphs( passed_data: dict, duration_data: dict) -> tuple: - """ + """Generates the data for the offcanvas displayed when a particular + point in a graph is clicked on. + + :param passed_data: The data from the clicked point in the graph + displaying the pass/fail data. + :param duration_data: The data from the clicked point in the graph + displaying the duration data. + :type passed_data: dict + :type duration data: dict + :returns: The data to be displayed on the offcanvas (job statistics + and the list of failed tests) and the information to show the + offcanvas. + :rtype: tuple(list, bool) """ if not (passed_data or duration_data):