From b2cb835b34c7404b2aaee3ec30700c67537da66d Mon Sep 17 00:00:00 2001 From: Tibor Frank Date: Wed, 26 Jan 2022 09:31:43 +0100 Subject: UTI: PoC - Dash application for Trending - delete old dash demo Change-Id: I3e75c1dc18ee85c1826838af0343a4b779f71754 Signed-off-by: Tibor Frank --- resources/tools/dash/app/pal/trending/dashboard.py | 69 ---- resources/tools/dash/app/pal/trending/data.py | 13 +- .../tools/dash/app/pal/trending/html_layout.txt | 29 ++ resources/tools/dash/app/pal/trending/layout.py | 364 +++++++++++++++++++-- .../dash/app/pal/trending/spec_test_selection.yaml | 106 ++++++ resources/tools/dash/app/pal/trending/trending.py | 74 +++++ 6 files changed, 553 insertions(+), 102 deletions(-) delete mode 100644 resources/tools/dash/app/pal/trending/dashboard.py create mode 100644 resources/tools/dash/app/pal/trending/html_layout.txt create mode 100644 resources/tools/dash/app/pal/trending/spec_test_selection.yaml create mode 100644 resources/tools/dash/app/pal/trending/trending.py (limited to 'resources/tools/dash/app/pal/trending') diff --git a/resources/tools/dash/app/pal/trending/dashboard.py b/resources/tools/dash/app/pal/trending/dashboard.py deleted file mode 100644 index ee5ea5123f..0000000000 --- a/resources/tools/dash/app/pal/trending/dashboard.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) 2022 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: -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Instantiate a Dash app.""" -import dash -from dash import dcc -from dash import html -from dash import dash_table -import numpy as np -import pandas as pd - -from .data import create_dataframe -from .layout import html_layout - - -def init_dashboard(server): - """Create a Plotly Dash dashboard. - - :param server: Flask server. - :type server: Flask - :returns: Dash app server. - :rtype: Dash - """ - dash_app = dash.Dash( - server=server, - routes_pathname_prefix="/trending/", - external_stylesheets=[ - "/static/dist/css/styles.css", - "https://fonts.googleapis.com/css?family=Lato", - ], - ) - - # Load DataFrame - df = create_dataframe() - - # Custom HTML layout - dash_app.index_string = html_layout - - # Create Layout - dash_app.layout = html.Div( - children=[ - create_data_table(df), - ], - id="dash-container", - ) - return dash_app.server - - -def create_data_table(df): - """Create Dash datatable from Pandas DataFrame.""" - table = dash_table.DataTable( - id="database-table", - columns=[{"name": i, "id": i} for i in df.columns], - data=df.to_dict("records"), - sort_action="native", - sort_mode="native", - page_size=300, - ) - return table diff --git a/resources/tools/dash/app/pal/trending/data.py b/resources/tools/dash/app/pal/trending/data.py index 06466a9175..298a40542e 100644 --- a/resources/tools/dash/app/pal/trending/data.py +++ b/resources/tools/dash/app/pal/trending/data.py @@ -11,14 +11,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Prepare data for Plotly Dash.""" +"""Prepare data for Plotly Dash. +""" import pandas as pd def create_dataframe(): - """Create Pandas DataFrame from local CSV.""" + """Create Pandas DataFrame from local CSV. + + DEMO + """ + return pd.read_csv( - "https://s3-docs.fd.io/csit/master/trending/_static/vpp/" - "csit-vpp-perf-mrr-daily-master-2n-zn2-trending.csv" + u"https://s3-docs.fd.io/csit/master/trending/_static/vpp/" + u"csit-vpp-perf-mrr-daily-master-2n-zn2-trending.csv" ) diff --git a/resources/tools/dash/app/pal/trending/html_layout.txt b/resources/tools/dash/app/pal/trending/html_layout.txt new file mode 100644 index 0000000000..3f0aa0cb8d --- /dev/null +++ b/resources/tools/dash/app/pal/trending/html_layout.txt @@ -0,0 +1,29 @@ + + + + {%metas%} + Continuous Performance Trending + {%favicon%} + {%css%} + + +
+ +
+ {%app_entry%} +
+ {%config%} + {%scripts%} + {%renderer%} +
+ + diff --git a/resources/tools/dash/app/pal/trending/layout.py b/resources/tools/dash/app/pal/trending/layout.py index 1f60aecd83..69ce0c416d 100644 --- a/resources/tools/dash/app/pal/trending/layout.py +++ b/resources/tools/dash/app/pal/trending/layout.py @@ -11,33 +11,339 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Plotly Dash HTML layout override.""" - -html_layout = """ - - - - {%metas%} - {%title%} - {%favicon%} - {%css%} - - -
- -
- {%app_entry%} - - - +"""Plotly Dash HTML layout override. """ + +import logging + +from dash import dcc +from dash import html +from dash import Input, Output, callback +from dash.exceptions import PreventUpdate +from yaml import load, FullLoader, YAMLError + + +class Layout: + """ + """ + + def __init__(self, app, html_layout_file, spec_file): + """ + """ + + # Inputs + self._app = app + self._html_layout_file = html_layout_file + self._spec_file = spec_file + + # Read from files: + self._html_layout = "" + self._spec_test = None + + try: + with open(self._html_layout_file, "r") as layout_file: + self._html_layout = layout_file.read() + except IOError as err: + logging.error(f"Not possible to open the file {layout_file}\n{err}") + + try: + with open(self._spec_file, "r") as file_read: + self._spec_test = load(file_read, Loader=FullLoader) + except IOError as err: + logging.error(f"Not possible to open the file {spec_file}\n{err}") + except YAMLError as err: + logging.error( + f"An error occurred while parsing the specification file " + f"{spec_file}\n" + f"{err}" + ) + + # Callbacks: + if self._app is not None and hasattr(self, 'callbacks'): + self.callbacks(self._app) + + # User choice (one test): + self._test_selection = { + "phy": "", + "area": "", + "test": "", + "core": "", + "frame-size": "", + "test-type": "" + } + + @property + def html_layout(self): + return self._html_layout + + @property + def spec_test(self): + return self._spec_test + + def _reset_test_selection(self): + self._test_selection = { + "phy": "", + "area": "", + "test": "", + "core": "", + "frame-size": "", + "test-type": "" + } + + def add_content(self): + """ + """ + if self._html_layout and self._spec_test: + return html.Div( + id="div-main", + children=[ + self._add_ctrl_div(), + self._add_plotting_div() + ] + ) + else: + return html.Div( + id="div-main-error", + children="An Error Occured." + ) + + def _add_ctrl_div(self): + """Add div with controls. It is placed on the left side. + """ + return html.Div( + id="div-controls", + children=[ + html.Div( + id="div-controls-tabs", + children=[ + self._add_ctrl_select(), + self._add_ctrl_shown() + ] + ) + ], + style={ + "display": "inline-block", + "width": "18%", + "padding": "5px" + } + ) + + def _add_plotting_div(self): + """Add div with plots and tables. It is placed on the right side. + """ + return html.Div( + id="div-plotting-area", + children=[ + # Only a visible note. + # TODO: Add content. + html.H3( + "Graphs and Tables", + style={ + "vertical-align": "middle", + "text-align": "center" + } + ) + ], + style={ + "vertical-align": "middle", + "display": "inline-block", + "width": "80%", + "padding": "5px" + } + ) + + def _add_ctrl_shown(self): + """ + """ + return html.Div( + id="div-ctrl-shown", + children="List of selected tests" + ) + + def _add_ctrl_select(self): + """ + """ + return html.Div( + id="div-ctrl-select", + children=[ + html.Br(), + html.Div( + children="Physical Test Bed Topology, NIC and Driver" + ), + dcc.Dropdown( + id="dd-ctrl-phy", + placeholder="Select a Physical Test Bed Topology...", + multi=False, + clearable=False, + options=[ + {"label": k, "value": k} for k in self._spec_test.keys() + ], + ), + html.Br(), + html.Div( + children="Area" + ), + dcc.Dropdown( + id="dd-ctrl-area", + placeholder="Select an Area...", + disabled=True, + multi=False, + clearable=False, + ), + html.Br(), + html.Div( + children="Test" + ), + dcc.Dropdown( + id="dd-ctrl-test", + placeholder="Select a Test...", + disabled=True, + multi=False, + clearable=False, + ), + + # Change to radio buttons: + html.Br(), + html.Div( + children="Number of Cores" + ), + dcc.Dropdown( + id="dd-ctrl-core", + placeholder="Select a Number of Cores...", + disabled=True, + multi=False, + clearable=False, + ), + html.Br(), + html.Div( + children="Frame Size" + ), + dcc.Dropdown( + id="dd-ctrl-framesize", + placeholder="Select a Frame Size...", + disabled=True, + multi=False, + clearable=False, + ), + html.Br(), + html.Div( + children="Test Type" + ), + dcc.Dropdown( + id="dd-ctrl-testtype", + placeholder="Select a Test Type...", + disabled=True, + multi=False, + clearable=False, + ), + html.Br(), + html.Button( + id="btn-ctrl-add", + children="Add", + disabled=True + ), + html.Br(), + html.Div( + id="div-ctrl-info" + ) + ] + ) + + def callbacks(self, app): + + @app.callback( + Output("dd-ctrl-area", "options"), + Output("dd-ctrl-area", "disabled"), + Input("dd-ctrl-phy", "value"), + ) + def _update_dd_area(phy): + """ + """ + + if phy is None: + raise PreventUpdate + + try: + options = [ + {"label": self._spec_test[phy][v]["label"], "value": v} + for v in [v for v in self._spec_test[phy].keys()] + ] + except KeyError: + options = list() + + return options, False + + @app.callback( + Output("dd-ctrl-test", "options"), + Output("dd-ctrl-test", "disabled"), + Input("dd-ctrl-phy", "value"), + Input("dd-ctrl-area", "value"), + ) + def _update_dd_test(phy, area): + """ + """ + + if not all((phy, area, )): + raise PreventUpdate + + try: + options = [ + {"label": v, "value": v} + for v in self._spec_test[phy][area]["test"] + ] + except KeyError: + options = list() + + return options, False + + @app.callback( + Output("btn-ctrl-add", "disabled"), + Input("dd-ctrl-phy", "value"), + Input("dd-ctrl-area", "value"), + Input("dd-ctrl-test", "value"), + ) + def _update_btn_add(phy, area, test): + """ + """ + + if all((phy, area, test, )): + self._test_selection["phy"] = phy + self._test_selection["area"] = area + self._test_selection["test"] = test + return False + else: + return True + + @app.callback( + Output("div-ctrl-info", "children"), + Output("dd-ctrl-phy", "value"), + Output("dd-ctrl-area", "value"), + Output("dd-ctrl-test", "value"), + Output("btn-ctrl-add", "n_clicks"), + Input("btn-ctrl-add", "n_clicks") + ) + def _print_user_selection(n_clicks): + """ + """ + + logging.info(f"\n\n{n_clicks}\n\n") + + if not n_clicks: + raise PreventUpdate + + selected = ( + f"{self._test_selection['phy']} # " + f"{self._test_selection['area']} # " + f"{self._test_selection['test']} # " + f"{n_clicks}\n" + ) + + self._reset_test_selection() + + return ( + selected, + None, + None, + None, + 0, + ) diff --git a/resources/tools/dash/app/pal/trending/spec_test_selection.yaml b/resources/tools/dash/app/pal/trending/spec_test_selection.yaml new file mode 100644 index 0000000000..dae902203a --- /dev/null +++ b/resources/tools/dash/app/pal/trending/spec_test_selection.yaml @@ -0,0 +1,106 @@ +2n-aws-nitro-50g-ena: + ip4-base: + label: IPv4 Routing Base + test: + - ethip4-ip4base + core: [1C, 2C] + frame-size: [64B, 1518B] + test-type: [MRR, NDR, PDR] + ip4-scale: + label: IPv4 Routing Scale + test: + - ethip4-ip4scale20k + - ethip4-ip4scale20k-rnd + core: [1C, 2C] + frame-size: [64B, 1518B] + test-type: [MRR, NDR, PDR] + ip6-base: + label: IPv6 Routing Base + test: + - ethip6-ip4base + core: [1C, 2C] + frame-size: [78B, 1518B] + test-type: [MRR, NDR, PDR] + ip6-scale: + label: IPv6 Routing Scale + test: + - ethip6-ip4scale20k + - ethip6-ip4scale20k-rnd + core: [1C, 2C] + frame-size: [78B, 1518B] + test-type: [MRR, NDR, PDR] +# 2n-clx-cx556a-rdma: +# 2n-clx-x710-avf: +# 2n-clx-x710-dpdk: +# 2n-clx-xxv710-af-xdp: +# 2n-clx-xxv710-avf: +# l2-base: +# l2-scale: +# ip4-base: +# ip4-scale: +# ip4-features: +# ip6-base: +# ip6-scale: +# ethip4-ethip4udpgeneve: +# nat44det-ip4-stl-bidir: +# nat44ed-ip4-stl-unidir: +# nat44ed-ip4-udp-stf-cps: +# nat44ed-ip4-tcp-stf-cps: +# nat44ed-ip4-udp-stf-pps: +# nat44ed-ip4-tcp-stf-pps: +# nat44ed-ip4-udp-tput: +# nat44ed-ip4-tcp-tput: +# vhost-base: +# memif-base: +# vnf-service-chains-routing: +# cnf-service-chains-routing: +# cnf-service-pipelines-routing: +# vnf-service-chains-tunnels: +# 2n-clx-xxv710-dpdk: +# 2n-dnv-x553-ixgbe: +# 2n-icx-xxv710-avf: +# 2n-icx-xxv710-dpdk: +# 2n-skx-x710-avf: +# 2n-skx-x710-dpdk: +# 2n-skx-xxv710-avf: +# 2n-skx-xxv710-dpdk: +# 2n-tx2-xl710-af-xdp: +# 2n-tx2-xl710-avf: +# 2n-tx2-xl710-dpdk: +# 2n-zn2-x710-avf: +# 2n-zn2-x710-dpdk: +# 2n-zn2-cx556a-rdma: +# 2n-zn2-xxv710-avf: +# 2n-zn2-xxv710-dpdk: +3n-aws-nitro-50g-ena: + ip4-base: + label: IPv4 Routing Base + test: + - ethip4-ip4base + core: [1C, 2C] + frame-size: [64B, 1518B] + test-type: [MRR, NDR, PDR] + + ip4-scale: + label: IPv4 Routing Scale + test: + - ethip4-ip4scale20k + - ethip4-ip4scale20k-rnd + core: [1C, 2C] + frame-size: [64B, 1518B] + test-type: [MRR, NDR, PDR] + + ipsec-base: + label: IPSec IPv4 Routing Base + test: + - ethip4ipsec40tnlsw-ip4base-int-aes256gcm + core: [1C, 2C] + frame-size: [IMIX, 1518B] + test-type: [MRR, NDR, PDR] +# 3n-dnv-x553-ixgbe: +# 3n-icx-xxv710-avf: +# 3n-icx-xxv710-dpdk: +# 3n-skx-x710-avf: +# 3n-skx-xxv710-avf: +# 3n-skx-xxv710-dpdk: +# 3n-tsh-x520-ixgbe: diff --git a/resources/tools/dash/app/pal/trending/trending.py b/resources/tools/dash/app/pal/trending/trending.py new file mode 100644 index 0000000000..4edf831045 --- /dev/null +++ b/resources/tools/dash/app/pal/trending/trending.py @@ -0,0 +1,74 @@ +# Copyright (c) 2022 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: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Instantiate the Trending Dash applocation. +""" +import dash +from dash import dcc +from dash import html +from dash import dash_table +import numpy as np +import pandas as pd + +from .data import create_dataframe +from .layout import Layout + + +def init_trending(server): + """Create a Plotly Dash dashboard. + + :param server: Flask server. + :type server: Flask + :returns: Dash app server. + :rtype: Dash + """ + + dash_app = dash.Dash( + server=server, + routes_pathname_prefix=u"/trending/", + external_stylesheets=[ + u"/static/dist/css/styles.css", + u"https://fonts.googleapis.com/css?family=Lato", + ], + ) + + # Load DataFrame + df = create_dataframe() + + # Custom HTML layout + layout = Layout( + app=dash_app, + html_layout_file="pal/trending/html_layout.txt", + spec_file="pal/trending/spec_test_selection.yaml" + ) + dash_app.index_string = layout.html_layout + dash_app.layout = layout.add_content() + + return dash_app.server + + +def create_data_table(df): + """Create Dash datatable from Pandas DataFrame. + + DEMO + """ + + table = dash_table.DataTable( + id=u"database-table", + columns=[{u"name": i, u"id": i} for i in df.columns], + data=df.to_dict(u"records"), + sort_action=u"native", + sort_mode=u"native", + page_size=10, + ) + return table -- cgit 1.2.3-korg