X-Git-Url: https://gerrit.fd.io/r/gitweb?a=blobdiff_plain;f=csit.infra.dash%2Fapp%2Fcdash%2Fcomparisons%2Flayout.py;h=d32542617ccd915f44308049c462d320444c5bbf;hb=540b27dbf9befcf589f5f572e8aac909f1738b51;hp=bb4c6dd93c0c1f17c05741559b748748881386fb;hpb=0fc5aff9887fa7a3125c71d0662475a3f9a763ba;p=csit.git diff --git a/csit.infra.dash/app/cdash/comparisons/layout.py b/csit.infra.dash/app/cdash/comparisons/layout.py index bb4c6dd93c..d32542617c 100644 --- a/csit.infra.dash/app/cdash/comparisons/layout.py +++ b/csit.infra.dash/app/cdash/comparisons/layout.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 Cisco and/or its affiliates. +# Copyright (c) 2024 Cisco and/or its affiliates. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: @@ -14,6 +14,8 @@ """Plotly Dash HTML layout override. """ + +import logging import pandas as pd import dash_bootstrap_components as dbc @@ -23,12 +25,14 @@ from dash import Input, Output, State from dash.exceptions import PreventUpdate from dash.dash_table.Format import Format, Scheme from ast import literal_eval +from yaml import load, FullLoader, YAMLError from ..utils.constants import Constants as C from ..utils.control_panel import ControlPanel from ..utils.trigger import Trigger from ..utils.url_processing import url_decode -from ..utils.utils import generate_options, gen_new_url +from ..utils.utils import generate_options, gen_new_url, navbar_report, \ + filter_table_data, show_tooltip from .tables import comparison_table @@ -53,7 +57,8 @@ CP_PARAMS = { "cmp-val-opt": list(), "cmp-val-dis": True, "cmp-val-val": str(), - "normalize-val": list() + "normalize-val": list(), + "outliers-val": list() } # List of comparable parameters. @@ -62,7 +67,7 @@ CMP_PARAMS = { "infra": "Infrastructure", "frmsize": "Frame Size", "core": "Number of Cores", - "ttype": "Test Type" + "ttype": "Measurement" } @@ -74,7 +79,8 @@ class Layout: self, app: Flask, data_iterative: pd.DataFrame, - html_layout_file: str + html_layout_file: str, + tooltip_file: str ) -> None: """Initialization: - save the input parameters, @@ -85,15 +91,19 @@ class Layout: :param data_iterative: Iterative data to be used in comparison tables. :param html_layout_file: Path and name of the file specifying the HTML layout of the dash application. + :param tooltip_file: Path and name of the yaml file specifying the + tooltips. :type app: Flask :type data_iterative: pandas.DataFrame :type html_layout_file: str + :type tooltip_file: str """ # Inputs self._app = app - self._html_layout_file = html_layout_file self._data = data_iterative + self._html_layout_file = html_layout_file + self._tooltip_file = tooltip_file # Get structure of tests: tbs = dict() @@ -142,7 +152,9 @@ class Layout: tbs[dut][dver][infra]["ttype"].append("MRR") elif row["test_type"] == "ndrpdr": if "NDR" not in tbs[dut][dver][infra]["ttype"]: - tbs[dut][dver][infra]["ttype"].extend(("NDR", "PDR", )) + tbs[dut][dver][infra]["ttype"].extend( + ("NDR", "PDR", "Latency") + ) elif row["test_type"] == "hoststack" and \ row["tg_type"] in ("iperf", "vpp"): if "BPS" not in tbs[dut][dver][infra]["ttype"]: @@ -162,6 +174,19 @@ class Layout: f"Not possible to open the file {self._html_layout_file}\n{err}" ) + try: + with open(self._tooltip_file, "r") as file_read: + self._tooltips = load(file_read, Loader=FullLoader) + except IOError as err: + logging.warning( + f"Not possible to open the file {self._tooltip_file}\n{err}" + ) + except YAMLError as err: + logging.warning( + f"An error occurred while parsing the specification file " + f"{self._tooltip_file}\n{err}" + ) + # Callbacks: if self._app is not None and hasattr(self, "callbacks"): self.callbacks(self._app) @@ -192,9 +217,7 @@ class Layout: dbc.Row( id="row-navbar", class_name="g-0", - children=[ - self._add_navbar() - ] + children=[navbar_report((False, True, False, False)), ] ), dbc.Row( id="row-main", @@ -202,10 +225,24 @@ class Layout: children=[ dcc.Store(id="store-control-panel"), dcc.Store(id="store-selected"), + dcc.Store(id="store-table-data"), + dcc.Store(id="store-filtered-table-data"), dcc.Location(id="url", refresh=False), self._add_ctrl_col(), self._add_plotting_col() ] + ), + dbc.Offcanvas( + class_name="w-75", + id="offcanvas-documentation", + title="Documentation", + placement="end", + is_open=False, + children=html.Iframe( + src=C.URL_DOC_REL_NOTES, + width="100%", + height="100%" + ) ) ] ) @@ -222,31 +259,6 @@ 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", - children=[ - dbc.NavItem( - dbc.NavLink( - C.COMP_TITLE, - disabled=True, - external_link=True, - href="#" - ) - ) - ], - brand=C.BRAND, - brand_href="/", - brand_external_link=True, - class_name="p-2", - fluid=True - ) - def _add_ctrl_col(self) -> dbc.Col: """Add column with controls. It is placed on the left side. @@ -297,7 +309,9 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText("DUT"), + dbc.InputGroupText( + show_tooltip(self._tooltips, "help-dut", "DUT") + ), dbc.Select( id={"type": "ctrl-dd", "index": "dut"}, placeholder="Select a Device under Test...", @@ -319,7 +333,11 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText("CSIT and DUT Version"), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-csit-dut", + "CSIT and DUT Version" + )), dbc.Select( id={"type": "ctrl-dd", "index": "dutver"}, placeholder="Select a CSIT and DUT Version...") @@ -333,7 +351,11 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText("Infra"), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-infra", + "Infra" + )), dbc.Select( id={"type": "ctrl-dd", "index": "infra"}, placeholder=\ @@ -349,7 +371,11 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText("Frame Size"), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-framesize", + "Frame Size" + )), dbc.Checklist( id={"type": "ctrl-cl", "index": "frmsize"}, inline=True, @@ -366,7 +392,11 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText("Number of Cores"), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-cores", + "Number of Cores" + )), dbc.Checklist( id={"type": "ctrl-cl", "index": "core"}, inline=True, @@ -383,7 +413,11 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText("Test Type"), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-measurement", + "Measurement" + )), dbc.Checklist( id={"type": "ctrl-cl", "index": "ttype"}, inline=True, @@ -403,7 +437,11 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText("Parameter"), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-cmp-parameter", + "Parameter" + )), dbc.Select( id={"type": "ctrl-dd", "index": "cmpprm"}, placeholder="Select a Parameter..." @@ -418,7 +456,11 @@ class Layout: children=[ dbc.InputGroup( [ - dbc.InputGroupText("Value"), + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-cmp-value", + "Value" + )), dbc.Select( id={"type": "ctrl-dd", "index": "cmpval"}, placeholder="Select a Value..." @@ -430,21 +472,33 @@ class Layout: ) ] - normalize = [ + processing = [ dbc.Row( class_name="g-0 p-1", children=[ dbc.InputGroup( - dbc.Checklist( - id="normalize", - options=[{ - "value": "normalize", - "label": "Normalize to 2GHz CPU frequency" - }], - value=[], - inline=True, - class_name="ms-2" - ), + children = [ + dbc.Checklist( + id="normalize", + options=[{ + "value": "normalize", + "label": "Normalize to 2GHz CPU frequency" + }], + value=[], + inline=True, + class_name="ms-2" + ), + dbc.Checklist( + id="outliers", + options=[{ + "value": "outliers", + "label": "Remove Extreme Outliers" + }], + value=[], + inline=True, + class_name="ms-2" + ) + ], style={"align-items": "center"}, size="sm" ) @@ -489,10 +543,10 @@ class Layout: dbc.Card( [ dbc.CardHeader( - html.H5("Normalization") + html.H5("Data Manipulations") ), dbc.CardBody( - children=normalize, + children=processing, class_name="g-0 p-0" ) ], @@ -503,28 +557,26 @@ class Layout: ) ] + @staticmethod def _get_plotting_area( - self, - selected: dict, - url: str, - normalize: bool + title: str, + table: pd.DataFrame, + url: str ) -> list: """Generate the plotting area with all its content. - :param selected: Selected parameters of tests. - :param normalize: If true, the values in tables are normalized. + :param title: The title of the comparison table. + :param table: Comparison table to be displayed. :param url: URL to be displayed in the modal window. - :type selected: dict - :type normalize: bool + :type title: str + :type table: pandas.DataFrame :type url: str :returns: List of rows with elements to be displayed in the plotting area. :rtype: list """ - title, df = comparison_table(self._data, selected, normalize) - - if df.empty: + if table.empty: return dbc.Row( dbc.Col( children=dbc.Alert( @@ -537,7 +589,7 @@ class Layout: ) cols = list() - for idx, col in enumerate(df.columns): + for idx, col in enumerate(table.columns): if idx == 0: cols.append({ "name": ["", col], @@ -566,11 +618,13 @@ class Layout: children=[ dbc.Col( children=dash_table.DataTable( + id={"type": "table", "index": "comparison"}, columns=cols, - data=df.to_dict("records"), + data=table.to_dict("records"), merge_duplicate_headers=True, - editable=True, - filter_action="native", + editable=False, + filter_action="custom", + filter_query="", sort_action="native", sort_mode="multi", selected_columns=[], @@ -613,7 +667,18 @@ class Layout: ), dbc.Button( id="plot-btn-download", - children="Download Data", + children="Download Table", + class_name="me-1", + color="info", + style={ + "text-transform": "none", + "padding": "0rem 1rem" + } + ), + dcc.Download(id="download-iterative-data"), + dbc.Button( + id="plot-btn-download-raw", + children="Download Raw Data", class_name="me-1", color="info", style={ @@ -621,7 +686,7 @@ class Layout: "padding": "0rem 1rem" } ), - dcc.Download(id="download-iterative-data") + dcc.Download(id="download-raw-data") ], className=\ "d-grid gap-0 d-md-flex justify-content-md-end" @@ -646,7 +711,10 @@ class Layout: [ Output("store-control-panel", "data"), Output("store-selected", "data"), + Output("store-table-data", "data"), + Output("store-filtered-table-data", "data"), Output("plotting-area", "children"), + Output({"type": "table", "index": ALL}, "data"), Output({"type": "ctrl-dd", "index": "dut"}, "value"), Output({"type": "ctrl-dd", "index": "dutver"}, "options"), Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"), @@ -666,15 +734,21 @@ class Layout: Output({"type": "ctrl-dd", "index": "cmpval"}, "options"), Output({"type": "ctrl-dd", "index": "cmpval"}, "disabled"), Output({"type": "ctrl-dd", "index": "cmpval"}, "value"), - Output("normalize", "value") + Output("normalize", "value"), + Output("outliers", "value") ], [ State("store-control-panel", "data"), - State("store-selected", "data") + State("store-selected", "data"), + State("store-table-data", "data"), + State("store-filtered-table-data", "data"), + State({"type": "table", "index": ALL}, "data") ], [ Input("url", "href"), Input("normalize", "value"), + Input("outliers", "value"), + Input({"type": "table", "index": ALL}, "filter_query"), Input({"type": "ctrl-dd", "index": ALL}, "value"), Input({"type": "ctrl-cl", "index": ALL}, "value"), Input({"type": "ctrl-btn", "index": ALL}, "n_clicks") @@ -683,8 +757,13 @@ class Layout: def _update_application( control_panel: dict, selected: dict, + store_table_data: list, + filtered_data: list, + table_data: list, href: str, normalize: list, + outliers: bool, + table_filter: str, *_ ) -> tuple: """Update the application when the event is detected. @@ -720,11 +799,15 @@ class Layout: r_sel = selected["reference"]["selection"] c_sel = selected["compare"] normalize = literal_eval(url_params["norm"][0]) + try: # Necessary for backward compatibility + outliers = literal_eval(url_params["outliers"][0]) + except (KeyError, IndexError, AttributeError): + outliers = list() process_url = bool( (selected["reference"]["set"] == True) and (c_sel["set"] == True) ) - except (KeyError, IndexError): + except (KeyError, IndexError, AttributeError): pass if process_url: ctrl_panel.set({ @@ -754,7 +837,8 @@ class Layout: [r_sel["infra"]]["ttype"] ), "ttype-val": r_sel["ttype"], - "normalize-val": normalize + "normalize-val": normalize, + "outliers-val": outliers }) opts = list() for itm, label in CMP_PARAMS.items(): @@ -783,6 +867,9 @@ class Layout: elif trigger.type == "normalize": ctrl_panel.set({"normalize-val": normalize}) on_draw = True + elif trigger.type == "outliers": + ctrl_panel.set({"outliers-val": outliers}) + on_draw = True elif trigger.type == "ctrl-dd": if trigger.idx == "dut": try: @@ -865,6 +952,8 @@ class Layout: for itm in ctrl_panel.get(f"{value}-opt"): set_val = ctrl_panel.get(f"{value}-val") if isinstance(set_val, list): + if itm["value"] == "Latency": + continue if itm["value"] not in set_val: opts.append(itm) else: @@ -900,8 +989,13 @@ class Layout: if all((ctrl_panel.get("core-val"), ctrl_panel.get("frmsize-val"), ctrl_panel.get("ttype-val"), )): + if "Latency" in ctrl_panel.get("ttype-val"): + ctrl_panel.set({"ttype-val": ["Latency", ]}) opts = list() for itm, label in CMP_PARAMS.items(): + if "Latency" in ctrl_panel.get("ttype-val") and \ + itm == "ttype": + continue if len(ctrl_panel.get(f"{itm}-opt")) > 1: if isinstance(ctrl_panel.get(f"{itm}-val"), list): if len(ctrl_panel.get(f"{itm}-opt")) == \ @@ -925,19 +1019,47 @@ class Layout: "cmp-val-dis": True, "cmp-val-val": str() }) + elif trigger.type == "table" and trigger.idx == "comparison": + filtered_data = filter_table_data( + store_table_data, + table_filter[0] + ) + table_data = [filtered_data, ] if all((on_draw, selected["reference"]["set"], selected["compare"]["set"], )): - plotting_area = self._get_plotting_area( + title, table = comparison_table( + data=self._data, selected=selected, - normalize=bool(normalize), + normalize=normalize, + format="html", + remove_outliers=outliers + ) + plotting_area = self._get_plotting_area( + title=title, + table=table, url=gen_new_url( parsed_url, - params={"selected": selected, "norm": normalize} + params={ + "selected": selected, + "norm": normalize, + "outliers": outliers + } ) ) + store_table_data = table.to_dict("records") + filtered_data = store_table_data + if table_data: + table_data = [store_table_data, ] - ret_val = [ctrl_panel.panel, selected, plotting_area] + ret_val = [ + ctrl_panel.panel, + selected, + store_table_data, + filtered_data, + plotting_area, + table_data + ] ret_val.extend(ctrl_panel.values) return ret_val @@ -955,28 +1077,75 @@ class Layout: @app.callback( Output("download-iterative-data", "data"), - State("store-selected", "data"), - State("normalize", "value"), + State("store-table-data", "data"), + State("store-filtered-table-data", "data"), Input("plot-btn-download", "n_clicks"), prevent_initial_call=True ) - def _download_trending_data(selected: dict, normalize: list, _: int): + def _download_comparison_data( + table_data: list, + filtered_table_data: list, + _: int + ) -> dict: """Download the data. - :param selected: List of tests selected by user stored in the - browser. - :param normalize: If set, the data is normalized to 2GHz CPU - frequency. - :type selected: list - :type normalize: list + :param table_data: Original unfiltered table data. + :param filtered_table_data: Filtered table data. + :type table_data: list + :type filtered_table_data: list :returns: dict of data frame content (base64 encoded) and meta data used by the Download component. :rtype: dict """ - if not selected: + if not table_data: raise PreventUpdate - _, table = comparison_table(self._data, selected, normalize, "csv") + if filtered_table_data: + table = pd.DataFrame.from_records(filtered_table_data) + else: + table = pd.DataFrame.from_records(table_data) return dcc.send_data_frame(table.to_csv, C.COMP_DOWNLOAD_FILE_NAME) + + @app.callback( + Output("download-raw-data", "data"), + State("store-selected", "data"), + Input("plot-btn-download-raw", "n_clicks"), + prevent_initial_call=True + ) + def _download_raw_comparison_data(selected: dict, _: int) -> dict: + """Download the data. + + :param selected: Selected tests. + :type selected: dict + :returns: dict of data frame content (base64 encoded) and meta data + used by the Download component. + :rtype: dict + """ + + if not selected: + raise PreventUpdate + + _, table = comparison_table( + data=self._data, + selected=selected, + normalize=False, + remove_outliers=False, + raw_data=True + ) + + return dcc.send_data_frame( + table.dropna(how="all", axis=1).to_csv, + f"raw_{C.COMP_DOWNLOAD_FILE_NAME}" + ) + + @app.callback( + Output("offcanvas-documentation", "is_open"), + Input("btn-documentation", "n_clicks"), + State("offcanvas-documentation", "is_open") + ) + def toggle_offcanvas_documentation(n_clicks, is_open): + if n_clicks: + return not is_open + return is_open