X-Git-Url: https://gerrit.fd.io/r/gitweb?p=csit.git;a=blobdiff_plain;f=resources%2Ftools%2Fdash%2Fapp%2Fpal%2Fnews%2Flayout.py;h=6e598315e55b9a364835e4fddfc8e94da0041f88;hp=2f66ce5c817f272cde1dfac8e6cc9082ae1bef4a;hb=1efb9c180524bdb0bdccc612e182364821810fa5;hpb=371bac71bc789bf9d68fa1b8ba77f21c4876244f diff --git a/resources/tools/dash/app/pal/news/layout.py b/resources/tools/dash/app/pal/news/layout.py index 2f66ce5c81..6e598315e5 100644 --- a/resources/tools/dash/app/pal/news/layout.py +++ b/resources/tools/dash/app/pal/news/layout.py @@ -27,26 +27,25 @@ from yaml import load, FullLoader, YAMLError from copy import deepcopy from ..data.data import Data -from ..data.utils import classify_anomalies -from .tables import table_news +from ..utils.constants import Constants as C +from ..utils.utils import classify_anomalies, show_tooltip, gen_new_url, \ + 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 .tables import table_news, table_summary class Layout: """The layout of the dash app and the callbacks. """ - # The default job displayed when the page is loaded first time. - DEFAULT_JOB = "csit-vpp-perf-mrr-daily-master-2n-icx" - - # Time period for regressions and progressions. - TIME_PERIOD = 21 # [days] - def __init__(self, app: Flask, html_layout_file: str, data_spec_file: str, tooltip_file: str) -> None: """Initialization: - save the input parameters, - read and pre-process the data, - - prepare data fro the control panel, + - prepare data for the control panel, - read HTML layout file, - read tooltips from the tooltip file. @@ -73,29 +72,29 @@ class Layout: data_stats, data_mrr, data_ndrpdr = Data( data_spec_file=self._data_spec_file, debug=True - ).read_stats(days=self.TIME_PERIOD) + ).read_stats(days=C.NEWS_TIME_PERIOD) df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True) # Prepare information for the control panel: - jobs = sorted(list(df_tst_info["job"].unique())) - job_info = { + self._jobs = sorted(list(df_tst_info["job"].unique())) + d_job_info = { "job": list(), "dut": list(), "ttype": list(), "cadence": list(), "tbed": list() } - for job in jobs: + for job in self._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(self.DEFAULT_JOB) + self._default = set_job_params(self.job_info, C.NEWS_DEFAULT_JOB) # Pre-process the data: @@ -119,7 +118,9 @@ class Layout: "regressions": list(), "progressions": list() } - for job in jobs: + logging.debug("Processing jobs ...") + for job in self._jobs: + logging.debug(f"+ {job}") # Create lists of failed tests: df_job = df_tst_info.loc[(df_tst_info["job"] == job)] last_build = max(df_job["build"].unique()) @@ -255,7 +256,8 @@ class Layout: f"{self._tooltip_file}\n{err}" ) - self._default_tab_failed = table_news(self.data, self._default["job"]) + self._default_tab_failed = \ + table_news(self.data, self._default["job"], C.NEWS_TIME_PERIOD) # Callbacks: if self._app is not None and hasattr(self, 'callbacks'): @@ -273,163 +275,6 @@ class Layout: def default(self) -> dict: return self._default - def _get_duts(self) -> list: - """Get the list of DUTs from the pre-processed information about jobs. - - :returns: Alphabeticaly sorted list of DUTs. - :rtype: list - """ - return sorted(list(self.df_job_info["dut"].unique())) - - def _get_ttypes(self, dut: str) -> list: - """Get the list of test types from the pre-processed information about - jobs. - - :param dut: The DUT for which the list of test types will be populated. - :type dut: str - :returns: Alphabeticaly sorted list of test types. - :rtype: 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: - """Get the list of cadences from the pre-processed information about - jobs. - - :param dut: The DUT for which the list of cadences will be populated. - :param ttype: The test type for which the list of cadences will be - populated. - :type dut: str - :type ttype: str - :returns: Alphabeticaly sorted list of cadences. - :rtype: list - """ - return sorted(list(self.df_job_info.loc[( - (self.df_job_info["dut"] == dut) & - (self.df_job_info["ttype"] == ttype) - )]["cadence"].unique())) - - def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list: - """Get the list of test beds from the pre-processed information about - jobs. - - :param dut: The DUT for which the list of test beds will be populated. - :param ttype: The test type for which the list of test beds will be - populated. - :param cadence: The cadence for which the list of test beds will be - populated. - :type dut: str - :type ttype: str - :type cadence: str - :returns: Alphabeticaly sorted list of test beds. - :rtype: 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())) - - def _get_job(self, dut, ttype, cadence, testbed): - """Get the name of a job defined by dut, ttype, cadence, test bed. - Input information comes from the control panel. - - :param dut: The DUT for which the job name will be created. - :param ttype: The test type for which the job name will be created. - :param cadence: The cadence for which the job name will be created. - :param testbed: The test bed for which the job name will be created. - :type dut: str - :type ttype: str - :type cadence: str - :type testbed: str - :returns: Job name. - :rtype: str - """ - 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() - - @staticmethod - def _generate_options(opts: list) -> list: - """Return list of options for radio items in control panel. The items in - the list are dictionaries with keys "label" and "value". - - :params opts: List of options (str) to be used for the generated list. - :type opts: list - :returns: List of options (dict). - :rtype: list - """ - return [{"label": i, "value": i} for i in opts] - - def _set_job_params(self, job: str) -> dict: - """Create a dictionary with all options and values for (and from) the - given job. - - :params job: The name of job for and from which the dictionary will be - created. - :type job: str - :returns: Dictionary with all options and values for (and from) the - given job. - :rtype: dict - """ - - 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 _show_tooltip(self, id: str, title: str, - clipboard_id: str=None) -> list: - """Generate list of elements to display a text (e.g. a title) with a - tooltip and optionaly with Copy&Paste icon and the clipboard - functionality enabled. - - :param id: Tooltip ID. - :param title: A text for which the tooltip will be displayed. - :param clipboard_id: If defined, a Copy&Paste icon is displayed and the - clipboard functionality is enabled. - :type id: str - :type title: str - :type clipboard_id: str - :returns: List of elements to display a text with a tooltip and - optionaly with Copy&Paste icon. - :rtype: list - """ - - return [ - dcc.Clipboard(target_id=clipboard_id, title="Copy URL") \ - if clipboard_id else str(), - f"{title} ", - dbc.Badge( - id=id, - children="?", - pill=True, - color="white", - text_color="info", - class_name="border ms-1", - ), - dbc.Tooltip( - children=self._tooltips.get(id, str()), - target=id, - placement="auto" - ) - ] - def add_content(self): """Top level method which generated the web page. @@ -440,7 +285,7 @@ class Layout: If no HTML layout is provided, an error message is displayed instead. - :returns: The HTML div with teh whole page. + :returns: The HTML div with the whole page. :rtype: html.Div """ @@ -449,6 +294,7 @@ class Layout: id="div-main", children=[ dcc.Store(id="control-panel"), + dcc.Location(id="url", refresh=False), dbc.Row( id="row-navbar", class_name="g-0", @@ -509,7 +355,7 @@ class Layout: """Add column with control panel. It is placed on the left side. :returns: Column with the control panel. - :rtype: dbc.col + :rtype: dbc.Col """ return dbc.Col( @@ -523,7 +369,7 @@ class Layout: """Add column with tables. It is placed on the right side. :returns: Column with tables. - :rtype: dbc.col + :rtype: dbc.Col """ return dbc.Col( @@ -533,6 +379,33 @@ class Layout: id="row-table-failed", class_name="g-0 p-2", children=self._default_tab_failed + ), + dbc.Row( + class_name="g-0 p-2", + align="center", + justify="start", + children=[ + dbc.InputGroup( + class_name="me-1", + children=[ + dbc.InputGroupText( + style=C.URL_STYLE, + children=show_tooltip( + self._tooltips, + "help-url", "URL", + "input-url" + ) + ), + dbc.Input( + id="input-url", + readonly=True, + type="url", + style=C.URL_STYLE, + value="" + ) + ] + ) + ] ) ], width=9, @@ -556,7 +429,7 @@ class Layout: children=[ dbc.Label( class_name="p-0", - children=self._show_tooltip( + children=show_tooltip(self._tooltips, "help-dut", "Device under Test") ), dbc.Row( @@ -574,7 +447,7 @@ class Layout: children=[ dbc.Label( class_name="p-0", - children=self._show_tooltip( + children=show_tooltip(self._tooltips, "help-ttype", "Test Type"), ), dbc.RadioItems( @@ -590,7 +463,7 @@ class Layout: children=[ dbc.Label( class_name="p-0", - children=self._show_tooltip( + children=show_tooltip(self._tooltips, "help-cadence", "Cadence"), ), dbc.RadioItems( @@ -606,7 +479,7 @@ class Layout: children=[ dbc.Label( class_name="p-0", - children=self._show_tooltip( + children=show_tooltip(self._tooltips, "help-tbed", "Test Bed"), ), dbc.Select( @@ -617,6 +490,25 @@ class Layout: ) ] ), + dbc.Row( + class_name="gy-1 p-0", + children=[ + dbc.ButtonGroup( + [ + dbc.Button( + id="btn-summary", + children=( + f"Show Summary from the last " + f"{C.NEWS_SUMMARY_PERIOD} Days" + ), + class_name="me-1", + color="info" + ) + ], + size="md", + ) + ] + ), dbc.Row( class_name="gy-1", children=[ @@ -628,16 +520,23 @@ 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 = { @@ -656,6 +555,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 @@ -671,16 +577,36 @@ 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: + """Returns the values from the Control panel as a list. + + :returns: The values from the Control panel. + :rtype: list + """ return list(self._panel.values()) def callbacks(self, app): + """Callbacks for the whole application. + + :param app: The application. + :type app: Flask + """ @app.callback( Output("control-panel", "data"), # Store Output("row-table-failed", "children"), + Output("input-url", "value"), Output("ri-ttypes", "options"), Output("ri-cadences", "options"), Output("dd-tbeds", "options"), @@ -694,23 +620,50 @@ class Layout: Input("ri-ttypes", "value"), Input("ri-cadences", "value"), Input("dd-tbeds", "value"), + Input("url", "href"), + Input("btn-summary", "n_clicks") ) - def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str, - tbed: str) -> tuple: - """ + def _update_application(cp_data: dict, dut: str, ttype: str, + cadence:str, tbed: str, href: str, btn_all: int) -> 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 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 href: str + :returns: New values for web page elements. + :rtype: tuple """ ctrl_panel = self.ControlPanel(cp_data, self.default) + # Parse the url: + parsed_url = url_decode(href) + if parsed_url: + url_params = parsed_url["params"] + else: + url_params = None + + show_summary = False + 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, @@ -722,11 +675,11 @@ 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"), + 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({ @@ -737,8 +690,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({ @@ -750,19 +703,42 @@ class Layout: ctrl_panel.set({ "dd-tbeds-value": tbed }) + elif trigger_id == "url": + # TODO: Add verification + if url_params: + new_job = url_params.get("job", list())[0] + if new_job and new_job != "all": + job_params = set_job_params(self.job_info, new_job) + ctrl_panel = self.ControlPanel(None, job_params) + if new_job and new_job == "all": + show_summary = True + else: + ctrl_panel = self.ControlPanel(cp_data, self.default) + elif trigger_id == "btn-summary": + show_summary = True - 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") - ) - ctrl_panel.set({"al-job-children": job}) - tab_failed = table_news(self.data, job) + if show_summary: + ctrl_panel.set({ + "al-job-children": \ + f"Summary from the last {C.NEWS_SUMMARY_PERIOD} days" + }) + job = "all" + tables = table_summary(self.data, self._jobs) + else: + 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") + ) + ctrl_panel.set({"al-job-children": job}) + tables = table_news(self.data, job, C.NEWS_TIME_PERIOD) ret_val = [ ctrl_panel.panel, - tab_failed + tables, + gen_new_url(parsed_url, {"job": job}) ] ret_val.extend(ctrl_panel.values()) return ret_val