aboutsummaryrefslogtreecommitdiffstats
path: root/csit.infra.dash
diff options
context:
space:
mode:
authorTibor Frank <tifrank@cisco.com>2024-02-20 11:04:48 +0000
committerTibor Frank <tifrank@cisco.com>2024-02-28 14:15:28 +0000
commit7d3054dede4f630e9b20ac0e69f029bea93bdf5f (patch)
tree1dfa9c41fd326a822a0aeefab8982a5fda61f051 /csit.infra.dash
parent0e11b36be7f77578ecab1fc6a4fb6ac16e615b8f (diff)
C-Dash: Add search in testsoper-240304
Change-Id: Ia250c4b4e299d48bc68cf01e65fe37a281047060 Signed-off-by: Tibor Frank <tifrank@cisco.com>
Diffstat (limited to 'csit.infra.dash')
-rw-r--r--csit.infra.dash/app/cdash/__init__.py13
-rw-r--r--csit.infra.dash/app/cdash/comparisons/__init__.py2
-rw-r--r--csit.infra.dash/app/cdash/comparisons/comparisons.py2
-rw-r--r--csit.infra.dash/app/cdash/comparisons/layout.py50
-rw-r--r--csit.infra.dash/app/cdash/comparisons/tables.py64
-rw-r--r--csit.infra.dash/app/cdash/coverage/__init__.py2
-rw-r--r--csit.infra.dash/app/cdash/coverage/coverage.py2
-rw-r--r--csit.infra.dash/app/cdash/coverage/layout.py45
-rw-r--r--csit.infra.dash/app/cdash/coverage/tables.py26
-rw-r--r--csit.infra.dash/app/cdash/data/__init__.py2
-rw-r--r--csit.infra.dash/app/cdash/data/data.py2
-rw-r--r--csit.infra.dash/app/cdash/news/__init__.py2
-rw-r--r--csit.infra.dash/app/cdash/news/layout.py50
-rw-r--r--csit.infra.dash/app/cdash/news/news.py2
-rw-r--r--csit.infra.dash/app/cdash/news/tables.py2
-rw-r--r--csit.infra.dash/app/cdash/report/__init__.py2
-rw-r--r--csit.infra.dash/app/cdash/report/graphs.py8
-rw-r--r--csit.infra.dash/app/cdash/report/layout.py159
-rw-r--r--csit.infra.dash/app/cdash/report/report.py2
-rw-r--r--csit.infra.dash/app/cdash/routes.py5
-rw-r--r--csit.infra.dash/app/cdash/search/__init__.py12
-rw-r--r--csit.infra.dash/app/cdash/search/layout.py928
-rw-r--r--csit.infra.dash/app/cdash/search/layout.yaml276
-rw-r--r--csit.infra.dash/app/cdash/search/search.py52
-rw-r--r--csit.infra.dash/app/cdash/search/tables.py123
-rw-r--r--csit.infra.dash/app/cdash/stats/__init__.py2
-rw-r--r--csit.infra.dash/app/cdash/stats/graphs.py2
-rw-r--r--csit.infra.dash/app/cdash/stats/layout.py45
-rw-r--r--csit.infra.dash/app/cdash/stats/stats.py2
-rw-r--r--csit.infra.dash/app/cdash/templates/base_layout.jinja25
-rw-r--r--csit.infra.dash/app/cdash/trending/__init__.py2
-rw-r--r--csit.infra.dash/app/cdash/trending/graphs.py4
-rw-r--r--csit.infra.dash/app/cdash/trending/layout.py134
-rw-r--r--csit.infra.dash/app/cdash/trending/trending.py2
-rw-r--r--csit.infra.dash/app/cdash/utils/__init__.py2
-rw-r--r--csit.infra.dash/app/cdash/utils/anomalies.py2
-rw-r--r--csit.infra.dash/app/cdash/utils/constants.py23
-rw-r--r--csit.infra.dash/app/cdash/utils/control_panel.py2
-rw-r--r--csit.infra.dash/app/cdash/utils/telemetry_data.py2
-rw-r--r--csit.infra.dash/app/cdash/utils/trigger.py2
-rw-r--r--csit.infra.dash/app/cdash/utils/url_processing.py2
-rw-r--r--csit.infra.dash/app/cdash/utils/utils.py396
-rw-r--r--csit.infra.dash/app/config.py2
-rw-r--r--csit.infra.dash/app/wsgi.py2
44 files changed, 1909 insertions, 557 deletions
diff --git a/csit.infra.dash/app/cdash/__init__.py b/csit.infra.dash/app/cdash/__init__.py
index 796dcef01f..3d3f2001a3 100644
--- a/csit.infra.dash/app/cdash/__init__.py
+++ b/csit.infra.dash/app/cdash/__init__.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:
@@ -127,6 +127,17 @@ def init_app():
from .coverage.coverage import init_coverage
app = init_coverage(app, data_coverage=data["coverage"])
+ if all((data["trending"].empty, data["iterative"].empty,
+ data["coverage"].empty)):
+ logging.error((
+ f'"{C.SEARCH_TITLE}" application not loaded, '
+ 'no data available.'
+ ))
+ else:
+ logging.info(C.SEARCH_TITLE)
+ from .search.search import init_search
+ app = init_search(app, data)
+
return app
diff --git a/csit.infra.dash/app/cdash/comparisons/__init__.py b/csit.infra.dash/app/cdash/comparisons/__init__.py
index f0d52c25b6..c6a5f639fe 100644
--- a/csit.infra.dash/app/cdash/comparisons/__init__.py
+++ b/csit.infra.dash/app/cdash/comparisons/__init__.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:
diff --git a/csit.infra.dash/app/cdash/comparisons/comparisons.py b/csit.infra.dash/app/cdash/comparisons/comparisons.py
index bc42085dc8..01319ada77 100644
--- a/csit.infra.dash/app/cdash/comparisons/comparisons.py
+++ b/csit.infra.dash/app/cdash/comparisons/comparisons.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:
diff --git a/csit.infra.dash/app/cdash/comparisons/layout.py b/csit.infra.dash/app/cdash/comparisons/layout.py
index 0680cc3d06..45bc75aca1 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:
@@ -28,8 +28,9 @@ 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 .tables import comparison_table, filter_table_data
+from ..utils.utils import generate_options, gen_new_url, navbar_report, \
+ filter_table_data
+from .tables import comparison_table
# Control panel partameters and their default values.
@@ -194,9 +195,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",
@@ -238,43 +237,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.REPORT_TITLE,
- external_link=True,
- href="/report"
- )),
- dbc.NavItem(dbc.NavLink(
- "Comparisons",
- active=True,
- external_link=True,
- href="/comparisons"
- )),
- dbc.NavItem(dbc.NavLink(
- "Coverage Data",
- external_link=True,
- href="/coverage"
- )),
- dbc.NavItem(dbc.NavLink(
- "Documentation",
- id="btn-documentation",
- ))
- ],
- 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.
@@ -539,7 +501,7 @@ class Layout:
) -> list:
"""Generate the plotting area with all its content.
- :param title: The title of the comparison table..
+ :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 title: str
diff --git a/csit.infra.dash/app/cdash/comparisons/tables.py b/csit.infra.dash/app/cdash/comparisons/tables.py
index 8c19d3c776..ab99f1839d 100644
--- a/csit.infra.dash/app/cdash/comparisons/tables.py
+++ b/csit.infra.dash/app/cdash/comparisons/tables.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:
@@ -309,65 +309,3 @@ def comparison_table(
)
return (title, df_cmp)
-
-
-def filter_table_data(
- store_table_data: list,
- table_filter: str
- ) -> list:
- """Filter table data using user specified filter.
-
- :param store_table_data: Table data represented as a list of records.
- :param table_filter: User specified filter.
- :type store_table_data: list
- :type table_filter: str
- :returns: A new table created by filtering of table data represented as
- a list of records.
- :rtype: list
- """
-
- # Checks:
- if not any((table_filter, store_table_data, )):
- return store_table_data
-
- def _split_filter_part(filter_part: str) -> tuple:
- """Split a part of filter into column name, operator and value.
- A "part of filter" is a sting berween "&&" operator.
-
- :param filter_part: A part of filter.
- :type filter_part: str
- :returns: Column name, operator, value
- :rtype: tuple[str, str, str|float]
- """
- for operator_type in C.OPERATORS:
- for operator in operator_type:
- if operator in filter_part:
- name_p, val_p = filter_part.split(operator, 1)
- name = name_p[name_p.find("{") + 1 : name_p.rfind("}")]
- val_p = val_p.strip()
- if (val_p[0] == val_p[-1] and val_p[0] in ("'", '"', '`')):
- value = val_p[1:-1].replace("\\" + val_p[0], val_p[0])
- else:
- try:
- value = float(val_p)
- except ValueError:
- value = val_p
-
- return name, operator_type[0].strip(), value
- return (None, None, None)
-
- df = pd.DataFrame.from_records(store_table_data)
- for filter_part in table_filter.split(" && "):
- col_name, operator, filter_value = _split_filter_part(filter_part)
- if operator == "contains":
- df = df.loc[df[col_name].str.contains(filter_value, regex=True)]
- elif operator in ("eq", "ne", "lt", "le", "gt", "ge"):
- # These operators match pandas series operator method names.
- df = df.loc[getattr(df[col_name], operator)(filter_value)]
- elif operator == "datestartswith":
- # This is a simplification of the front-end filtering logic,
- # only works with complete fields in standard format.
- # Currently not used in comparison tables.
- df = df.loc[df[col_name].str.startswith(filter_value)]
-
- return df.to_dict("records")
diff --git a/csit.infra.dash/app/cdash/coverage/__init__.py b/csit.infra.dash/app/cdash/coverage/__init__.py
index f0d52c25b6..c6a5f639fe 100644
--- a/csit.infra.dash/app/cdash/coverage/__init__.py
+++ b/csit.infra.dash/app/cdash/coverage/__init__.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:
diff --git a/csit.infra.dash/app/cdash/coverage/coverage.py b/csit.infra.dash/app/cdash/coverage/coverage.py
index 4dfd7a80de..3388d48ad8 100644
--- a/csit.infra.dash/app/cdash/coverage/coverage.py
+++ b/csit.infra.dash/app/cdash/coverage/coverage.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:
diff --git a/csit.infra.dash/app/cdash/coverage/layout.py b/csit.infra.dash/app/cdash/coverage/layout.py
index a2d51d46a3..8ebda5e127 100644
--- a/csit.infra.dash/app/cdash/coverage/layout.py
+++ b/csit.infra.dash/app/cdash/coverage/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:
@@ -29,7 +29,7 @@ from ast import literal_eval
from ..utils.constants import Constants as C
from ..utils.control_panel import ControlPanel
from ..utils.trigger import Trigger
-from ..utils.utils import label, gen_new_url, generate_options
+from ..utils.utils import label, gen_new_url, generate_options, navbar_report
from ..utils.url_processing import url_decode
from .tables import coverage_tables, select_coverage_data
@@ -161,9 +161,7 @@ class Layout:
dbc.Row(
id="row-navbar",
class_name="g-0",
- children=[
- self._add_navbar()
- ]
+ children=[navbar_report((False, False, True, False)), ]
),
dbc.Row(
id="row-main",
@@ -203,43 +201,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.REPORT_TITLE,
- external_link=True,
- href="/report"
- )),
- dbc.NavItem(dbc.NavLink(
- "Comparisons",
- external_link=True,
- href="/comparisons"
- )),
- dbc.NavItem(dbc.NavLink(
- "Coverage Data",
- active=True,
- external_link=True,
- href="/coverage"
- )),
- dbc.NavItem(dbc.NavLink(
- "Documentation",
- id="btn-documentation",
- ))
- ],
- 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.
diff --git a/csit.infra.dash/app/cdash/coverage/tables.py b/csit.infra.dash/app/cdash/coverage/tables.py
index 372a8206bf..84adb091a1 100644
--- a/csit.infra.dash/app/cdash/coverage/tables.py
+++ b/csit.infra.dash/app/cdash/coverage/tables.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:
@@ -57,7 +57,7 @@ def select_coverage_data(
topo, arch, nic, drv = phy
drv_str = "" if drv == "dpdk" else drv.replace("_", "-")
else:
- return l_data
+ return l_data, None
df = pd.DataFrame(data.loc[(
(data["passed"] == True) &
@@ -78,8 +78,10 @@ def select_coverage_data(
df[df.test_id.str.contains(f"-{driver}-")].index,
inplace=True
)
-
- ttype = df["test_type"].to_list()[0]
+ try:
+ ttype = df["test_type"].to_list()[0]
+ except IndexError:
+ return l_data, None
# Prepare the coverage data
def _latency(hdrh_string: str, percentile: float) -> int:
@@ -177,16 +179,20 @@ def select_coverage_data(
def coverage_tables(
data: pd.DataFrame,
selected: dict,
- show_latency: bool=True
- ) -> list:
+ show_latency: bool=True,
+ start_collapsed: bool=True
+ ) -> dbc.Accordion:
"""Generate an accordion with coverage tables.
:param data: Coverage data.
:param selected: Dictionary with user selection.
:param show_latency: If True, latency is displayed in the tables.
+ :param start_collapsed: If True, the accordion with tables is collapsed when
+ displayed.
:type data: pandas.DataFrame
:type selected: dict
:type show_latency: bool
+ :type start_collapsed: bool
:returns: Accordion with suite names (titles) and tables.
:rtype: dash_bootstrap_components.Accordion
"""
@@ -295,9 +301,15 @@ def coverage_tables(
)
)
)
+ if not accordion_items:
+ accordion_items.append(dbc.AccordionItem(
+ title="No data.",
+ children="No data."
+ ))
+ start_collapsed = True
return dbc.Accordion(
children=accordion_items,
class_name="gy-1 p-0",
- start_collapsed=True,
+ start_collapsed=start_collapsed,
always_open=True
)
diff --git a/csit.infra.dash/app/cdash/data/__init__.py b/csit.infra.dash/app/cdash/data/__init__.py
index f0d52c25b6..c6a5f639fe 100644
--- a/csit.infra.dash/app/cdash/data/__init__.py
+++ b/csit.infra.dash/app/cdash/data/__init__.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:
diff --git a/csit.infra.dash/app/cdash/data/data.py b/csit.infra.dash/app/cdash/data/data.py
index ce98476897..2c49992bf8 100644
--- a/csit.infra.dash/app/cdash/data/data.py
+++ b/csit.infra.dash/app/cdash/data/data.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:
diff --git a/csit.infra.dash/app/cdash/news/__init__.py b/csit.infra.dash/app/cdash/news/__init__.py
index f0d52c25b6..c6a5f639fe 100644
--- a/csit.infra.dash/app/cdash/news/__init__.py
+++ b/csit.infra.dash/app/cdash/news/__init__.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:
diff --git a/csit.infra.dash/app/cdash/news/layout.py b/csit.infra.dash/app/cdash/news/layout.py
index 3f2280ef10..b40db48605 100644
--- a/csit.infra.dash/app/cdash/news/layout.py
+++ b/csit.infra.dash/app/cdash/news/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:
@@ -24,7 +24,7 @@ from dash import callback_context
from dash import Input, Output, State
from ..utils.constants import Constants as C
-from ..utils.utils import gen_new_url
+from ..utils.utils import gen_new_url, navbar_trending
from ..utils.anomalies import classify_anomalies
from ..utils.url_processing import url_decode
from .tables import table_summary
@@ -262,9 +262,7 @@ class Layout:
dbc.Row(
id="row-navbar",
class_name="g-0",
- children=[
- self._add_navbar()
- ]
+ children=[navbar_trending((False, True, False, False))]
),
dbc.Row(
id="row-main",
@@ -301,44 +299,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.TREND_TITLE,
- external_link=True,
- href="/trending"
- )),
- dbc.NavItem(dbc.NavLink(
- C.NEWS_TITLE,
- active=True,
- external_link=True,
- href="/news"
- )),
- dbc.NavItem(dbc.NavLink(
- C.STATS_TITLE,
- external_link=True,
- href="/stats"
- )),
- dbc.NavItem(dbc.NavLink(
- "Documentation",
- id="btn-documentation",
- ))
- ],
- 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 control panel. It is placed on the left side.
@@ -527,8 +487,8 @@ class Layout:
@app.callback(
Output("plot-mod-url", "is_open"),
- [Input("plot-btn-url", "n_clicks")],
- [State("plot-mod-url", "is_open")],
+ Input("plot-btn-url", "n_clicks"),
+ State("plot-mod-url", "is_open")
)
def toggle_plot_mod_url(n, is_open):
"""Toggle the modal window with url.
diff --git a/csit.infra.dash/app/cdash/news/news.py b/csit.infra.dash/app/cdash/news/news.py
index b5cc5483a8..747facc1bb 100644
--- a/csit.infra.dash/app/cdash/news/news.py
+++ b/csit.infra.dash/app/cdash/news/news.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:
diff --git a/csit.infra.dash/app/cdash/news/tables.py b/csit.infra.dash/app/cdash/news/tables.py
index c84f84e6f7..1e9aefaf04 100644
--- a/csit.infra.dash/app/cdash/news/tables.py
+++ b/csit.infra.dash/app/cdash/news/tables.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:
diff --git a/csit.infra.dash/app/cdash/report/__init__.py b/csit.infra.dash/app/cdash/report/__init__.py
index f0d52c25b6..c6a5f639fe 100644
--- a/csit.infra.dash/app/cdash/report/__init__.py
+++ b/csit.infra.dash/app/cdash/report/__init__.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:
diff --git a/csit.infra.dash/app/cdash/report/graphs.py b/csit.infra.dash/app/cdash/report/graphs.py
index 50a3be287a..44c57d4183 100644
--- a/csit.infra.dash/app/cdash/report/graphs.py
+++ b/csit.infra.dash/app/cdash/report/graphs.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:
@@ -73,8 +73,8 @@ def select_iterative_data(data: pd.DataFrame, itm:dict) -> pd.DataFrame:
return df
-def graph_iterative(data: pd.DataFrame, sel:dict, layout: dict,
- normalize: bool) -> tuple:
+def graph_iterative(data: pd.DataFrame, sel: list, layout: dict,
+ normalize: bool=False) -> tuple:
"""Generate the statistical box graph with iterative data (MRR, NDR and PDR,
for PDR also Latencies).
@@ -84,7 +84,7 @@ def graph_iterative(data: pd.DataFrame, sel:dict, layout: dict,
:param normalize: If True, the data is normalized to CPU frequency
Constants.NORM_FREQUENCY.
:param data: pandas.DataFrame
- :param sel: dict
+ :param sel: list
:param layout: dict
:param normalize: bool
:returns: Tuple of graphs - throughput and latency.
diff --git a/csit.infra.dash/app/cdash/report/layout.py b/csit.infra.dash/app/cdash/report/layout.py
index 08a430bc3b..400fd60f38 100644
--- a/csit.infra.dash/app/cdash/report/layout.py
+++ b/csit.infra.dash/app/cdash/report/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:
@@ -32,7 +32,8 @@ from ..utils.constants import Constants as C
from ..utils.control_panel import ControlPanel
from ..utils.trigger import Trigger
from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
- generate_options, get_list_group_items, graph_hdrh_latency
+ generate_options, get_list_group_items, navbar_report, \
+ show_iterative_graph_data
from ..utils.url_processing import url_decode
from .graphs import graph_iterative, select_iterative_data
@@ -250,9 +251,7 @@ class Layout:
dbc.Row(
id="row-navbar",
class_name="g-0",
- children=[
- self._add_navbar()
- ]
+ children=[navbar_report((True, False, False, False)), ]
),
dbc.Row(
id="row-main",
@@ -306,43 +305,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.REPORT_TITLE,
- active=True,
- external_link=True,
- href="/report"
- )),
- dbc.NavItem(dbc.NavLink(
- "Comparisons",
- external_link=True,
- href="/comparisons"
- )),
- dbc.NavItem(dbc.NavLink(
- "Coverage Data",
- external_link=True,
- href="/coverage"
- )),
- dbc.NavItem(dbc.NavLink(
- "Documentation",
- id="btn-documentation",
- ))
- ],
- 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.
@@ -1365,118 +1327,11 @@ class Layout:
"""
trigger = Trigger(callback_context.triggered)
-
- if trigger.idx == "tput":
- idx = 0
- elif trigger.idx == "bandwidth":
- idx = 1
- elif trigger.idx == "lat":
- idx = len(graph_data) - 1
- else:
- return list(), list(), False
-
- try:
- graph_data = graph_data[idx]["points"]
- except (IndexError, KeyError, ValueError, TypeError):
+ if not trigger.value:
raise PreventUpdate
- def _process_stats(data: list, param: str) -> list:
- """Process statistical data provided by plot.ly box graph.
-
- :param data: Statistical data provided by plot.ly box graph.
- :param param: Parameter saying if the data come from "tput" or
- "lat" graph.
- :type data: list
- :type param: str
- :returns: Listo of tuples where the first value is the
- statistic's name and the secont one it's value.
- :rtype: list
- """
- if len(data) == 7:
- stats = ("max", "upper fence", "q3", "median", "q1",
- "lower fence", "min")
- elif len(data) == 9:
- stats = ("outlier", "max", "upper fence", "q3", "median",
- "q1", "lower fence", "min", "outlier")
- elif len(data) == 1:
- if param == "lat":
- stats = ("average latency at 50% PDR", )
- elif param == "bandwidth":
- stats = ("bandwidth", )
- else:
- stats = ("throughput", )
- else:
- return list()
- unit = " [us]" if param == "lat" else str()
- return [(f"{stat}{unit}", f"{value['y']:,.0f}")
- for stat, value in zip(stats, data)]
-
- customdata = graph_data[0].get("customdata", dict())
- datapoint = customdata.get("metadata", dict())
- hdrh_data = customdata.get("hdrh", dict())
-
- list_group_items = list()
- for k, v in datapoint.items():
- if k == "csit-ref":
- if len(graph_data) > 1:
- continue
- list_group_item = dbc.ListGroupItem([
- dbc.Badge(k),
- html.A(v, href=f"{C.URL_JENKINS}{v}", target="_blank")
- ])
- else:
- list_group_item = dbc.ListGroupItem([dbc.Badge(k), v])
- list_group_items.append(list_group_item)
-
- graph = list()
- if trigger.idx == "tput":
- title = "Throughput"
- elif trigger.idx == "bandwidth":
- title = "Bandwidth"
- elif trigger.idx == "lat":
- title = "Latency"
- if len(graph_data) == 1:
- if hdrh_data:
- graph = [dbc.Card(
- class_name="gy-2 p-0",
- children=[
- dbc.CardHeader(hdrh_data.pop("name")),
- dbc.CardBody(dcc.Graph(
- id="hdrh-latency-graph",
- figure=graph_hdrh_latency(
- hdrh_data, self._graph_layout
- )
- ))
- ])
- ]
- else:
- raise PreventUpdate
-
- for k, v in _process_stats(graph_data, trigger.idx):
- list_group_items.append(dbc.ListGroupItem([dbc.Badge(k), v]))
-
- metadata = [
- dbc.Card(
- class_name="gy-2 p-0",
- children=[
- dbc.CardHeader(children=[
- dcc.Clipboard(
- target_id="tput-lat-metadata",
- title="Copy",
- style={"display": "inline-block"}
- ),
- title
- ]),
- dbc.CardBody(
- dbc.ListGroup(list_group_items, flush=True),
- id="tput-lat-metadata",
- class_name="p-0"
- )
- ]
- )
- ]
-
- return metadata, graph, True
+ return show_iterative_graph_data(
+ trigger, graph_data, self._graph_layout)
@app.callback(
Output("offcanvas-documentation", "is_open"),
diff --git a/csit.infra.dash/app/cdash/report/report.py b/csit.infra.dash/app/cdash/report/report.py
index 661bb2ce7f..ce5e977f4c 100644
--- a/csit.infra.dash/app/cdash/report/report.py
+++ b/csit.infra.dash/app/cdash/report/report.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:
diff --git a/csit.infra.dash/app/cdash/routes.py b/csit.infra.dash/app/cdash/routes.py
index 301738c643..ed29fffa12 100644
--- a/csit.infra.dash/app/cdash/routes.py
+++ b/csit.infra.dash/app/cdash/routes.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:
@@ -33,5 +33,6 @@ def home():
comp_title=C.COMP_TITLE,
stats_title=C.STATS_TITLE,
news_title=C.NEWS_TITLE,
- cov_title=C.COVERAGE_TITLE
+ cov_title=C.COVERAGE_TITLE,
+ search_title=C.SEARCH_TITLE
)
diff --git a/csit.infra.dash/app/cdash/search/__init__.py b/csit.infra.dash/app/cdash/search/__init__.py
new file mode 100644
index 0000000000..c6a5f639fe
--- /dev/null
+++ b/csit.infra.dash/app/cdash/search/__init__.py
@@ -0,0 +1,12 @@
+# 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:
+#
+# 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.
diff --git a/csit.infra.dash/app/cdash/search/layout.py b/csit.infra.dash/app/cdash/search/layout.py
new file mode 100644
index 0000000000..2c50fba352
--- /dev/null
+++ b/csit.infra.dash/app/cdash/search/layout.py
@@ -0,0 +1,928 @@
+# 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:
+#
+# 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.
+
+
+"""Plotly Dash HTML layout override.
+"""
+
+import logging
+import pandas as pd
+import dash_bootstrap_components as dbc
+
+from flask import Flask
+from dash import dcc
+from dash import html, dash_table
+from dash import callback_context, no_update, ALL
+from dash import Input, Output, State
+from dash.exceptions import PreventUpdate
+from yaml import load, FullLoader, YAMLError
+from ast import literal_eval
+
+from ..utils.constants import Constants as C
+from ..utils.control_panel import ControlPanel
+from ..utils.trigger import Trigger
+from ..utils.utils import gen_new_url, generate_options, navbar_trending, \
+ filter_table_data, show_trending_graph_data, show_iterative_graph_data
+from ..utils.url_processing import url_decode
+from .tables import search_table
+from ..coverage.tables import coverage_tables
+from ..report.graphs import graph_iterative
+from ..trending.graphs import graph_trending
+
+
+# Control panel partameters and their default values.
+CP_PARAMS = {
+ "datatype-val": str(),
+ "dut-opt": list(),
+ "dut-dis": C.STYLE_DONT_DISPLAY,
+ "dut-val": str(),
+ "release-opt": list(),
+ "release-dis": C.STYLE_DONT_DISPLAY,
+ "release-val": str(),
+ "help-dis": C.STYLE_DONT_DISPLAY,
+ "help-val": str(),
+ "search-dis": C.STYLE_DONT_DISPLAY,
+ "search-val": str()
+}
+
+
+class Layout:
+ """The layout of the dash app and the callbacks.
+ """
+
+ def __init__(self,
+ app: Flask,
+ data: dict,
+ html_layout_file: str,
+ graph_layout_file: str,
+ tooltip_file: str
+ ) -> None:
+ """Initialization:
+ - save the input parameters,
+ - read and pre-process the data,
+ - prepare data for the control panel,
+ - read HTML layout file,
+ - read graph layout file,
+ - read tooltips from the tooltip file.
+
+ :param app: Flask application running the dash application.
+ :param data_trending: Pandas dataframe with trending data.
+ :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 tooltip_file: Path and name of the yaml file specifying the
+ tooltips.
+ :type app: Flask
+ :type data_trending: pandas.DataFrame
+ :type html_layout_file: str
+ :type graph_layout_file: str
+ :type tooltip_file: str
+ """
+
+ # Inputs
+ self._app = app
+ self._html_layout_file = html_layout_file
+ self._graph_layout_file = graph_layout_file
+ self._tooltip_file = tooltip_file
+ # Inputs - Data
+ self._data = {
+ k: v for k, v in data.items() if not v.empty and k != "statistics"
+ }
+
+ for data_type, pd in self._data.items():
+ if pd.empty:
+ continue
+ full_id = list()
+
+ for _, row in pd.iterrows():
+ l_id = row["test_id"].split(".")
+ suite = l_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
+ replace("2n-", "")
+ tb = "-".join(row["job"].split("-")[-2:])
+ nic = suite.split("-")[0]
+ for driver in C.DRIVERS:
+ if driver in suite:
+ drv = driver
+ break
+ else:
+ drv = "dpdk"
+ test = l_id[-1]
+
+ if data_type in ("iterative", "coverage", ):
+ full_id.append(
+ "_".join((row["release"], row["dut_type"],
+ row["dut_version"], tb, nic, drv, test))
+ )
+ else: # Trending
+ full_id.append(
+ "_".join((row["dut_type"], tb, nic, drv, test))
+ )
+ pd["full_id"] = full_id
+
+ # Get structure of tests:
+ self._duts = dict()
+ for data_type, pd in self._data.items():
+ if pd.empty:
+ continue
+ self._duts[data_type] = dict()
+ if data_type in ("iterative", "coverage", ):
+ cols = ["job", "dut_type", "dut_version", "release", "test_id"]
+ for _, row in pd[cols].drop_duplicates().iterrows():
+ dut = row["dut_type"]
+ if self._duts[data_type].get(dut, None) is None:
+ self._duts[data_type][dut] = list()
+ if row["release"] not in self._duts[data_type][dut]:
+ self._duts[data_type][dut].append(row["release"])
+ else:
+ for dut in pd["dut_type"].unique():
+ if self._duts[data_type].get(dut, None) is None:
+ self._duts[data_type][dut] = list()
+
+ # Read from files:
+ self._html_layout = str()
+ self._graph_layout = None
+ self._tooltips = dict()
+
+ try:
+ with open(self._html_layout_file, "r") as file_read:
+ self._html_layout = file_read.read()
+ except IOError as err:
+ raise RuntimeError(
+ f"Not possible to open the file {self._html_layout_file}\n{err}"
+ )
+
+ try:
+ with open(self._graph_layout_file, "r") as file_read:
+ self._graph_layout = load(file_read, Loader=FullLoader)
+ except IOError as err:
+ raise RuntimeError(
+ f"Not possible to open the file {self._graph_layout_file}\n"
+ f"{err}"
+ )
+ except YAMLError as err:
+ raise RuntimeError(
+ f"An error occurred while parsing the specification file "
+ f"{self._graph_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)
+
+ @property
+ def html_layout(self):
+ return self._html_layout
+
+ def add_content(self):
+ """Top level method which generated the web page.
+
+ It generates:
+ - Store for user input data,
+ - Navigation bar,
+ - Main area with control panel and ploting area.
+
+ If no HTML layout is provided, an error message is displayed instead.
+
+ :returns: The HTML div with the whole page.
+ :rtype: html.Div
+ """
+ if self.html_layout and self._duts:
+ return html.Div(
+ id="div-main",
+ className="small",
+ children=[
+ dcc.Store(id="store"),
+ dcc.Store(id="store-table-data"),
+ dcc.Store(id="store-filtered-table-data"),
+ dcc.Location(id="url", refresh=False),
+ dbc.Row(
+ id="row-navbar",
+ class_name="g-0",
+ children=[navbar_trending((False, False, False, True))]
+ ),
+ dbc.Row(
+ id="row-main",
+ class_name="g-0",
+ children=[
+ self._add_ctrl_col(),
+ self._add_plotting_col()
+ ]
+ ),
+ dbc.Spinner(
+ dbc.Offcanvas(
+ class_name="w-75",
+ id="offcanvas-details",
+ title="Test Details",
+ placement="end",
+ is_open=False,
+ children=[]
+ ),
+ delay_show=C.SPINNER_DELAY
+ ),
+ dbc.Spinner(
+ dbc.Offcanvas(
+ class_name="w-50",
+ id="offcanvas-metadata",
+ title="Detailed Information",
+ placement="end",
+ is_open=False,
+ children=[
+ dbc.Row(id="metadata-tput-lat"),
+ dbc.Row(id="metadata-hdrh-graph")
+ ]
+ ),
+ delay_show=C.SPINNER_DELAY
+ ),
+ dbc.Offcanvas(
+ class_name="w-75",
+ id="offcanvas-documentation",
+ title="Documentation",
+ placement="end",
+ is_open=False,
+ children=html.Iframe(
+ src=C.URL_DOC_TRENDING,
+ width="100%",
+ height="100%"
+ )
+ )
+ ]
+ )
+ else:
+ return html.Div(
+ dbc.Alert("An Error Occured", color="danger"),
+ id="div-main-error"
+ )
+
+ 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(html.Div(self._add_ctrl_panel(), className="sticky-top"))
+
+ def _add_ctrl_panel(self) -> list:
+ """Add control panel.
+
+ :returns: Control panel.
+ :rtype: list
+ """
+ return [
+ dbc.Row(
+ class_name="g-0 p-1",
+ children=[
+ dbc.InputGroup(
+ [
+ dbc.InputGroupText("Data Type"),
+ dbc.Select(
+ id={"type": "ctrl-dd", "index": "datatype"},
+ placeholder="Select a Data Type...",
+ options=sorted(
+ [
+ {"label": k, "value": k} \
+ for k in self._data.keys()
+ ],
+ key=lambda d: d["label"]
+ )
+ )
+ ],
+ size="sm"
+ )
+ ],
+ style=C.STYLE_DISPLAY
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ id={"type": "ctrl-row", "index": "dut"},
+ children=[
+ dbc.InputGroup(
+ [
+ dbc.InputGroupText("DUT"),
+ dbc.Select(
+ id={"type": "ctrl-dd", "index": "dut"},
+ placeholder="Select a Device under Test..."
+ )
+ ],
+ size="sm"
+ )
+ ],
+ style=C.STYLE_DONT_DISPLAY
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ id={"type": "ctrl-row", "index": "release"},
+ children=[
+ dbc.InputGroup(
+ [
+ dbc.InputGroupText("Release"),
+ dbc.Select(
+ id={"type": "ctrl-dd", "index": "release"},
+ placeholder="Select a Release..."
+ )
+ ],
+ size="sm"
+ )
+ ],
+ style=C.STYLE_DONT_DISPLAY
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ id={"type": "ctrl-row", "index": "help"},
+ children=[
+ dbc.Input(
+ id={"type": "ctrl-dd", "index": "help"},
+ readonly=True,
+ debounce=True,
+ size="sm"
+ )
+ ],
+ style=C.STYLE_DONT_DISPLAY
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ id={"type": "ctrl-row", "index": "search"},
+ children=[
+ dbc.Input(
+ id={"type": "ctrl-dd", "index": "search"},
+ placeholder="Type a Regular Expression...",
+ debounce=True,
+ size="sm"
+ )
+ ],
+ style=C.STYLE_DONT_DISPLAY
+ )
+ ]
+
+ def _add_plotting_col(self) -> dbc.Col:
+ """Add column with tables. It is placed on the right side.
+
+ :returns: Column with tables.
+ :rtype: dbc.Col
+ """
+ return dbc.Col(
+ id="col-plotting-area",
+ children=[
+ dbc.Spinner(
+ children=[
+ dbc.Row(
+ id="plotting-area",
+ class_name="g-0 p-0",
+ children=[C.PLACEHOLDER, ]
+ )
+ ]
+ )
+ ],
+ width=9
+ )
+
+ @staticmethod
+ def _get_plotting_area(table: pd.DataFrame, url: str) -> list:
+ """Generate the plotting area with all its content.
+
+ :param table: Search table to be displayed.
+ :param url: URL to be displayed in a modal window.
+ :type table: pandas.DataFrame
+ :type url: str
+ :returns: List of rows with elements to be displayed in the plotting
+ area.
+ :rtype: list
+ """
+
+ if table.empty:
+ return dbc.Row(
+ dbc.Col(
+ children=dbc.Alert(
+ "No data found.",
+ color="danger"
+ ),
+ class_name="g-0 p-1",
+ ),
+ class_name="g-0 p-0"
+ )
+
+ columns = [{"name": col, "id": col} for col in table.columns]
+
+ return [
+ dbc.Row(
+ children=[
+ dbc.Col(
+ children=dash_table.DataTable(
+ id={"type": "table", "index": "search"},
+ columns=columns,
+ data=table.to_dict("records"),
+ filter_action="custom",
+ sort_action="native",
+ sort_mode="multi",
+ selected_columns=[],
+ selected_rows=[],
+ page_action="none",
+ style_cell={"textAlign": "left"}
+ ),
+ class_name="g-0 p-1"
+ )
+ ],
+ class_name="g-0 p-0"
+ ),
+ dbc.Row(
+ [
+ dbc.Col([html.Div(
+ [
+ dbc.Button(
+ id="plot-btn-url",
+ children="Show URL",
+ class_name="me-1",
+ color="info",
+ style={
+ "text-transform": "none",
+ "padding": "0rem 1rem"
+ }
+ ),
+ dbc.Modal(
+ [
+ dbc.ModalHeader(dbc.ModalTitle("URL")),
+ dbc.ModalBody(url)
+ ],
+ id="plot-mod-url",
+ size="xl",
+ is_open=False,
+ scrollable=True
+ ),
+ dbc.Button(
+ id="plot-btn-download",
+ children="Download Data",
+ class_name="me-1",
+ color="info",
+ style={
+ "text-transform": "none",
+ "padding": "0rem 1rem"
+ }
+ ),
+ dcc.Download(id="download-data")
+ ],
+ className=\
+ "d-grid gap-0 d-md-flex justify-content-md-end"
+ )])
+ ],
+ class_name="g-0 p-0"
+ ),
+ dbc.Row(
+ children=C.PLACEHOLDER,
+ class_name="g-0 p-1"
+ )
+ ]
+
+ def callbacks(self, app):
+ """Callbacks for the whole application.
+
+ :param app: The application.
+ :type app: Flask
+ """
+
+ @app.callback(
+ Output("store", "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": "datatype"}, "value"),
+ Output({"type": "ctrl-dd", "index": "dut"}, "options"),
+ Output({"type": "ctrl-row", "index": "dut"}, "style"),
+ Output({"type": "ctrl-dd", "index": "dut"}, "value"),
+ Output({"type": "ctrl-dd", "index": "release"}, "options"),
+ Output({"type": "ctrl-row", "index": "release"}, "style"),
+ Output({"type": "ctrl-dd", "index": "release"}, "value"),
+ Output({"type": "ctrl-row", "index": "help"}, "style"),
+ Output({"type": "ctrl-dd", "index": "help"}, "value"),
+ Output({"type": "ctrl-row", "index": "search"}, "style"),
+ Output({"type": "ctrl-dd", "index": "search"}, "value"),
+ State("store", "data"),
+ State("store-table-data", "data"),
+ State("store-filtered-table-data", "data"),
+ State({"type": "table", "index": ALL}, "data"),
+ Input("url", "href"),
+ Input({"type": "table", "index": ALL}, "filter_query"),
+ Input({"type": "ctrl-dd", "index": ALL}, "value"),
+ prevent_initial_call=True
+ )
+ def _update_application(
+ store: dict,
+ store_table_data: list,
+ filtered_data: list,
+ table_data: list,
+ href: str,
+ *_
+ ) -> tuple:
+ """Update the application when the event is detected.
+ """
+
+ if store is None:
+ store = {
+ "control-panel": dict(),
+ "selection": dict()
+ }
+
+ ctrl_panel = ControlPanel(
+ CP_PARAMS,
+ store.get("control-panel", dict())
+ )
+ selection = store["selection"]
+
+ plotting_area = no_update
+ on_draw = False
+
+ # Parse the url:
+ parsed_url = url_decode(href)
+ if parsed_url:
+ url_params = parsed_url["params"]
+ else:
+ url_params = None
+
+ trigger = Trigger(callback_context.triggered)
+ if trigger.type == "url" and url_params:
+ try:
+ selection = literal_eval(url_params["selection"][0])
+ if selection:
+ dtype = selection["datatype"]
+ dut = selection["dut"]
+ if dtype == "trending":
+ rls_opts = list()
+ rls_dis = C.STYLE_DONT_DISPLAY
+ else:
+ rls_opts = generate_options(self._duts[dtype][dut])
+ rls_dis = C.STYLE_DISPLAY
+ ctrl_panel.set({
+ "datatype-val": dtype,
+ "dut-opt": \
+ generate_options(self._duts[dtype].keys()),
+ "dut-dis": C.STYLE_DISPLAY,
+ "dut-val": dut,
+ "release-opt": rls_opts,
+ "release-dis": rls_dis,
+ "release-val": selection["release"],
+ "help-dis": C.STYLE_DISPLAY,
+ "help-val": selection["help"],
+ "search-dis": C.STYLE_DISPLAY,
+ "search-val": selection["regexp"]
+ })
+ on_draw = True
+ except (KeyError, IndexError, AttributeError, ValueError):
+ pass
+ elif trigger.type == "ctrl-dd":
+ if trigger.idx == "datatype":
+ try:
+ data_type = self._duts[trigger.value]
+ options = generate_options(data_type.keys())
+ disabled = C.STYLE_DISPLAY
+ except KeyError:
+ options = list()
+ disabled = C.STYLE_DONT_DISPLAY
+ ctrl_panel.set({
+ "datatype-val": trigger.value,
+ "dut-opt": options,
+ "dut-dis": disabled,
+ "dut-val": str(),
+ "release-opt": list(),
+ "release-dis": C.STYLE_DONT_DISPLAY,
+ "release-val": str(),
+ "help-dis": C.STYLE_DONT_DISPLAY,
+ "help-val": str(),
+ "search-dis": C.STYLE_DONT_DISPLAY,
+ "search-val": str()
+ })
+ elif trigger.idx == "dut":
+ try:
+ data_type = ctrl_panel.get("datatype-val")
+ dut = self._duts[data_type][trigger.value]
+ if data_type != "trending":
+ options = generate_options(dut)
+ disabled = C.STYLE_DISPLAY
+ except KeyError:
+ options = list()
+ disabled = C.STYLE_DONT_DISPLAY
+ if data_type == "trending":
+ ctrl_panel.set({
+ "dut-val": trigger.value,
+ "release-opt": list(),
+ "release-dis": C.STYLE_DONT_DISPLAY,
+ "release-val": str(),
+ "help-dis": disabled,
+ "help-val": "<testbed> <nic> <driver> " + \
+ "<framesize> <cores> <test>",
+ "search-dis": disabled,
+ "search-val": str()
+ })
+ else:
+ ctrl_panel.set({
+ "dut-val": trigger.value,
+ "release-opt": options,
+ "release-dis": disabled,
+ "release-val": str(),
+ "help-dis": C.STYLE_DONT_DISPLAY,
+ "help-val": str(),
+ "search-dis": C.STYLE_DONT_DISPLAY,
+ "search-val": str()
+ })
+ elif trigger.idx == "release":
+ ctrl_panel.set({
+ "release-val": trigger.value,
+ "help-dis": C.STYLE_DISPLAY,
+ "help-val": "<DUT version> <testbed> <nic> " + \
+ "<driver> <framesize> <core> <test>",
+ "search-dis": C.STYLE_DISPLAY,
+ "search-val": str()
+ })
+ elif trigger.idx == "search":
+ ctrl_panel.set({"search-val": trigger.value})
+ selection = {
+ "datatype": ctrl_panel.get("datatype-val"),
+ "dut": ctrl_panel.get("dut-val"),
+ "release": ctrl_panel.get("release-val"),
+ "help": ctrl_panel.get("help-val"),
+ "regexp": ctrl_panel.get("search-val"),
+ }
+ on_draw = True
+ elif trigger.type == "table" and trigger.idx == "search":
+ filtered_data = filter_table_data(
+ store_table_data,
+ trigger.value
+ )
+ table_data = [filtered_data, ]
+
+ if on_draw:
+ table = search_table(data=self._data, selection=selection)
+ plotting_area = Layout._get_plotting_area(
+ table,
+ gen_new_url(parsed_url, {"selection": selection})
+ )
+ store_table_data = table.to_dict("records")
+ filtered_data = store_table_data
+ if table_data:
+ table_data = [store_table_data, ]
+ else:
+ plotting_area = no_update
+
+ store["control-panel"] = ctrl_panel.panel
+ store["selection"] = selection
+ ret_val = [
+ store,
+ store_table_data,
+ filtered_data,
+ plotting_area,
+ table_data
+ ]
+ ret_val.extend(ctrl_panel.values)
+
+ return ret_val
+
+ @app.callback(
+ Output("offcanvas-details", "is_open"),
+ Output("offcanvas-details", "children"),
+ State("store", "data"),
+ State("store-filtered-table-data", "data"),
+ Input({"type": "table", "index": ALL}, "active_cell"),
+ prevent_initial_call=True
+ )
+ def show_test_data(store, table, *_):
+ """Show offcanvas with graphs and tables based on selected test(s).
+ """
+
+ trigger = Trigger(callback_context.triggered)
+ if not trigger.value:
+ raise PreventUpdate
+
+ try:
+ row = pd.DataFrame.from_records(table).\
+ iloc[[trigger.value["row"]]]
+ datatype = store["selection"]["datatype"]
+ dut = store["selection"]["dut"]
+ rls = store["selection"]["release"]
+ tb = row["Test Bed"].iloc[0]
+ nic = row["NIC"].iloc[0]
+ driver = row['Driver'].iloc[0]
+ test_name = row['Test'].iloc[0]
+ dutver = str()
+ except(KeyError, IndexError, AttributeError, ValueError):
+ raise PreventUpdate
+
+ data = self._data[datatype]
+ if datatype == "trending":
+ df = pd.DataFrame(data.loc[data["dut_type"] == dut])
+ else:
+ dutver = row["DUT Version"].iloc[0]
+ df = pd.DataFrame(data.loc[(
+ (data["dut_type"] == dut) &
+ (data["dut_version"] == dutver) &
+ (data["release"] == rls)
+ )])
+
+ df = df[df.full_id.str.contains(
+ f".*{tb}.*{nic}.*{test_name}",
+ regex=True
+ )]
+
+ if datatype in ("trending", "iterative"):
+ l_test_id = df["test_id"].iloc[0].split(".")
+ if dut == "dpdk":
+ area = "dpdk"
+ else:
+ area = ".".join(l_test_id[3:-2])
+ for drv in C.DRIVERS:
+ if drv in test_name:
+ test = test_name.replace(f"{drv}-", "")
+ break
+ else:
+ test = test_name
+ l_test = test.split("-")
+ testtype = l_test[-1]
+ if testtype == "ndrpdr":
+ testtype = ["ndr", "pdr"]
+ else:
+ testtype = [testtype, ]
+ core = l_test[1] if l_test[1] else "8c"
+ test = "-".join(l_test[2: -1])
+ test_id = f"{tb}-{nic}-{driver}-{core}-{l_test[0]}-{test}"
+ title = dbc.Row(
+ class_name="g-0 p-0",
+ children=dbc.Alert(test_id, color="info"),
+ )
+ selected = list()
+ indexes = ("tput", "bandwidth", "lat")
+ if datatype == "trending":
+ for ttype in testtype:
+ selected.append({
+ "id": f"{dut}-{test_id}-{ttype}",
+ "dut": dut,
+ "phy": f"{tb}-{nic}-{driver}",
+ "area": area,
+ "test": test,
+ "framesize": l_test[0],
+ "core": core,
+ "testtype": ttype
+ })
+ graphs = graph_trending(df, selected, self._graph_layout)
+ labels = ("Throughput", "Bandwidth", "Latency")
+ tabs = list()
+ for graph, label, idx in zip(graphs, labels, indexes):
+ if graph:
+ tabs.append(dbc.Tab(
+ children=dcc.Graph(
+ figure=graph,
+ id={"type": "graph-trend", "index": idx},
+ ),
+ label=label
+ ))
+ if tabs:
+ ret_val = [
+ title,
+ dbc.Row(dbc.Tabs(tabs), class_name="g-0 p-0")
+ ]
+ else:
+ ret_val = [
+ title,
+ dbc.Row("No data.", class_name="g-0 p-0")
+ ]
+
+ else: # Iterative
+ for ttype in testtype:
+ selected.append({
+ "id": f"{test_id}-{ttype}",
+ "rls": rls,
+ "dut": dut,
+ "dutver": dutver,
+ "phy": f"{tb}-{nic}-{driver}",
+ "area": area,
+ "test": test,
+ "framesize": l_test[0],
+ "core": core,
+ "testtype": ttype
+ })
+ graphs = graph_iterative(df, selected, self._graph_layout)
+ cols = list()
+ for graph, idx in zip(graphs, indexes):
+ if graph:
+ cols.append(dbc.Col(dcc.Graph(
+ figure=graph,
+ id={"type": "graph-iter", "index": idx},
+ )))
+ if not cols:
+ cols="No data."
+ ret_val = [
+ title,
+ dbc.Row(class_name="g-0 p-0", children=cols)
+ ]
+
+ elif datatype == "coverage":
+ ret_val = coverage_tables(
+ data=df,
+ selected={
+ "rls": rls,
+ "dut": dut,
+ "dutver": dutver,
+ "phy": f"{tb}-{nic}-{driver}",
+ "area": ".*",
+ },
+ start_collapsed=False
+ )
+ else:
+ raise PreventUpdate
+
+ return True, ret_val
+
+ @app.callback(
+ Output("metadata-tput-lat", "children"),
+ Output("metadata-hdrh-graph", "children"),
+ Output("offcanvas-metadata", "is_open"),
+ Input({"type": "graph-trend", "index": ALL}, "clickData"),
+ Input({"type": "graph-iter", "index": ALL}, "clickData"),
+ prevent_initial_call=True
+ )
+ def _show_metadata_from_trend_graph(
+ trend_data: dict,
+ iter_data: dict
+ ) -> tuple:
+ """Generates the data for the offcanvas displayed when a particular
+ point in a graph is clicked on.
+ """
+
+ trigger = Trigger(callback_context.triggered)
+ if not trigger.value:
+ raise PreventUpdate
+
+ if trigger.type == "graph-trend":
+ return show_trending_graph_data(
+ trigger, trend_data, self._graph_layout)
+ elif trigger.type == "graph-iter":
+ return show_iterative_graph_data(
+ trigger, iter_data, self._graph_layout)
+ else:
+ raise PreventUpdate
+
+ @app.callback(
+ Output("plot-mod-url", "is_open"),
+ Input("plot-btn-url", "n_clicks"),
+ State("plot-mod-url", "is_open")
+ )
+ def toggle_plot_mod_url(n, is_open):
+ """Toggle the modal window with url.
+ """
+ if n:
+ return not is_open
+ return is_open
+
+ @app.callback(
+ Output("download-data", "data"),
+ State("store-filtered-table-data", "data"),
+ Input("plot-btn-download", "n_clicks"),
+ prevent_initial_call=True
+ )
+ def _download_search_data(selection, _):
+ """Download the data.
+
+ :param selection: Selected data in table format (records).
+ :type selection: dict
+ :returns: dict of data frame content (base64 encoded) and meta data
+ used by the Download component.
+ :rtype: dict
+ """
+
+ if not selection:
+ raise PreventUpdate
+
+ return dcc.send_data_frame(
+ pd.DataFrame.from_records(selection).to_csv,
+ C.SEARCH_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
diff --git a/csit.infra.dash/app/cdash/search/layout.yaml b/csit.infra.dash/app/cdash/search/layout.yaml
new file mode 100644
index 0000000000..7d86e53f67
--- /dev/null
+++ b/csit.infra.dash/app/cdash/search/layout.yaml
@@ -0,0 +1,276 @@
+plot-throughput:
+ xaxis:
+ title: "Test Cases [Index]"
+ autorange: True
+ fixedrange: False
+ gridcolor: "rgb(230, 230, 230)"
+ linecolor: "rgb(220, 220, 220)"
+ linewidth: 1
+ showgrid: True
+ showline: True
+ showticklabels: True
+ tickcolor: "rgb(220, 220, 220)"
+ tickmode: "array"
+ zeroline: True
+ yaxis:
+ title: "Throughput [pps|cps|rps|bps]"
+ gridcolor: "rgb(230, 230, 230)"
+ hoverformat: ".3s"
+ tickformat: ".3s"
+ linecolor: "rgb(220, 220, 220)"
+ linewidth: 1
+ showgrid: True
+ showline: True
+ showticklabels: True
+ tickcolor: "rgb(220, 220, 220)"
+ zeroline: True
+ range: [0, 100]
+ autosize: True
+ margin:
+ t: 50
+ b: 0
+ l: 80
+ r: 20
+ showlegend: False
+ height: 850
+ paper_bgcolor: "#fff"
+ plot_bgcolor: "#fff"
+ hoverlabel:
+ namelength: -1
+
+plot-bandwidth:
+ xaxis:
+ title: "Test Cases [Index]"
+ autorange: True
+ fixedrange: False
+ gridcolor: "rgb(230, 230, 230)"
+ linecolor: "rgb(220, 220, 220)"
+ linewidth: 1
+ showgrid: True
+ showline: True
+ showticklabels: True
+ tickcolor: "rgb(220, 220, 220)"
+ tickmode: "array"
+ zeroline: True
+ yaxis:
+ title: "Bandwidth [bps]"
+ gridcolor: "rgb(230, 230, 230)"
+ hoverformat: ".3s"
+ tickformat: ".3s"
+ linecolor: "rgb(220, 220, 220)"
+ linewidth: 1
+ showgrid: True
+ showline: True
+ showticklabels: True
+ tickcolor: "rgb(220, 220, 220)"
+ zeroline: True
+ range: [0, 200]
+ autosize: True
+ margin:
+ t: 50
+ b: 0
+ l: 80
+ r: 20
+ showlegend: False
+ height: 850
+ paper_bgcolor: "#fff"
+ plot_bgcolor: "#fff"
+ hoverlabel:
+ namelength: -1
+
+plot-latency:
+ xaxis:
+ title: "Test Cases [Index]"
+ autorange: True
+ fixedrange: False
+ gridcolor: "rgb(230, 230, 230)"
+ linecolor: "rgb(220, 220, 220)"
+ linewidth: 1
+ showgrid: True
+ showline: True
+ showticklabels: True
+ tickcolor: "rgb(220, 220, 220)"
+ tickmode: "array"
+ zeroline: True
+ yaxis:
+ title: "Average Latency at 50% PDR [us]"
+ gridcolor: "rgb(230, 230, 230)"
+ hoverformat: ".3s"
+ tickformat: ".3s"
+ linecolor: "rgb(220, 220, 220)"
+ linewidth: 1
+ showgrid: True
+ showline: True
+ showticklabels: True
+ tickcolor: "rgb(220, 220, 220)"
+ zeroline: True
+ range: [0, 200]
+ autosize: True
+ margin:
+ t: 50
+ b: 0
+ l: 80
+ r: 20
+ showlegend: False
+ height: 850
+ paper_bgcolor: "#fff"
+ plot_bgcolor: "#fff"
+ hoverlabel:
+ namelength: -1
+
+plot-trending-tput:
+ autosize: True
+ showlegend: False
+ yaxis:
+ showticklabels: True
+ tickformat: ".3s"
+ title: "Throughput [pps|cps|rps|bps]"
+ hoverformat: ".5s"
+ gridcolor: "rgb(238, 238, 238)"
+ linecolor: "rgb(238, 238, 238)"
+ showline: True
+ zeroline: False
+ tickcolor: "rgb(238, 238, 238)"
+ linewidth: 1
+ showgrid: True
+ xaxis:
+ title: 'Date [MMDD]'
+ type: "date"
+ autorange: True
+ fixedrange: False
+ showgrid: True
+ gridcolor: "rgb(238, 238, 238)"
+ showline: True
+ linecolor: "rgb(238, 238, 238)"
+ zeroline: False
+ linewidth: 1
+ showticklabels: True
+ tickcolor: "rgb(238, 238, 238)"
+ tickmode: "auto"
+ tickformat: "%m%d"
+ margin:
+ r: 20
+ b: 0
+ t: 5
+ l: 70
+ paper_bgcolor: "#fff"
+ plot_bgcolor: "#fff"
+ hoverlabel:
+ namelength: -1
+
+plot-trending-bandwidth:
+ autosize: True
+ showlegend: False
+ yaxis:
+ showticklabels: True
+ tickformat: ".3s"
+ title: "Bandwidth [bps]"
+ hoverformat: ".5s"
+ gridcolor: "rgb(238, 238, 238)"
+ linecolor: "rgb(238, 238, 238)"
+ showline: True
+ zeroline: False
+ tickcolor: "rgb(238, 238, 238)"
+ linewidth: 1
+ showgrid: True
+ xaxis:
+ title: 'Date [MMDD]'
+ type: "date"
+ autorange: True
+ fixedrange: False
+ showgrid: True
+ gridcolor: "rgb(238, 238, 238)"
+ showline: True
+ linecolor: "rgb(238, 238, 238)"
+ zeroline: False
+ linewidth: 1
+ showticklabels: True
+ tickcolor: "rgb(238, 238, 238)"
+ tickmode: "auto"
+ tickformat: "%m%d"
+ margin:
+ r: 20
+ b: 0
+ t: 5
+ l: 70
+ paper_bgcolor: "#fff"
+ plot_bgcolor: "#fff"
+ hoverlabel:
+ namelength: -1
+
+plot-trending-lat:
+ autosize: True
+ showlegend: False
+ yaxis:
+ showticklabels: True
+ tickformat: ".3s"
+ title: "Average Latency at 50% PDR [us]"
+ hoverformat: ".5s"
+ gridcolor: "rgb(238, 238, 238)"
+ linecolor: "rgb(238, 238, 238)"
+ showline: True
+ zeroline: False
+ tickcolor: "rgb(238, 238, 238)"
+ linewidth: 1
+ showgrid: True
+ xaxis:
+ title: 'Date [MMDD]'
+ type: "date"
+ autorange: True
+ fixedrange: False
+ showgrid: True
+ gridcolor: "rgb(238, 238, 238)"
+ showline: True
+ linecolor: "rgb(238, 238, 238)"
+ zeroline: False
+ linewidth: 1
+ showticklabels: True
+ tickcolor: "rgb(238, 238, 238)"
+ tickmode: "auto"
+ tickformat: "%m%d"
+ margin:
+ r: 20
+ b: 0
+ t: 5
+ l: 70
+ paper_bgcolor: "#fff"
+ plot_bgcolor: "#fff"
+ hoverlabel:
+ namelength: -1
+
+plot-hdrh-latency:
+ showlegend: True
+ legend:
+ traceorder: "normal"
+ orientation: "h"
+ xanchor: "left"
+ yanchor: "top"
+ x: 0
+ y: -0.25
+ bgcolor: "rgba(255, 255, 255, 0)"
+ bordercolor: "rgba(255, 255, 255, 0)"
+ xaxis:
+ type: "log"
+ title: "Percentile [%]"
+ autorange: True
+ gridcolor: "rgb(230, 230, 230)"
+ linecolor: "rgb(220, 220, 220)"
+ linewidth: 1
+ showgrid: True
+ showline: True
+ showticklabels: True
+ tickcolor: "rgb(220, 220, 220)"
+ tickvals: [1, 2, 1e1, 20, 1e2, 1e3, 1e4, 1e5, 1e6]
+ ticktext: [0, 50, 90, 95, 99, 99.9, 99.99, 99.999, 99.9999]
+ yaxis:
+ title: "One-Way Latency per Direction [us]"
+ gridcolor: "rgb(230, 230, 230)"
+ linecolor: "rgb(220, 220, 220)"
+ linewidth: 1
+ showgrid: True
+ showline: True
+ showticklabels: True
+ tickcolor: "rgb(220, 220, 220)"
+ autosize: True
+ paper_bgcolor: "white"
+ plot_bgcolor: "white"
diff --git a/csit.infra.dash/app/cdash/search/search.py b/csit.infra.dash/app/cdash/search/search.py
new file mode 100644
index 0000000000..0ecdcb7f82
--- /dev/null
+++ b/csit.infra.dash/app/cdash/search/search.py
@@ -0,0 +1,52 @@
+# 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:
+#
+# 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 Search Dash application.
+"""
+
+import dash
+
+from ..utils.constants import Constants as C
+from .layout import Layout
+
+
+def init_search(
+ server,
+ data: tuple
+ ) -> dash.Dash:
+ """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=C.SEARCH_ROUTES_PATHNAME_PREFIX,
+ external_stylesheets=C.EXTERNAL_STYLESHEETS,
+ title=C.SEARCH_TITLE
+ )
+
+ layout = Layout(
+ app=dash_app,
+ data=data,
+ html_layout_file=C.HTML_LAYOUT_FILE,
+ graph_layout_file=C.SEARCH_GRAPH_LAYOUT_FILE,
+ tooltip_file=C.TOOLTIP_FILE
+ )
+ dash_app.index_string = layout.html_layout
+ dash_app.layout = layout.add_content()
+
+ return dash_app.server
diff --git a/csit.infra.dash/app/cdash/search/tables.py b/csit.infra.dash/app/cdash/search/tables.py
new file mode 100644
index 0000000000..a5ffd76d3d
--- /dev/null
+++ b/csit.infra.dash/app/cdash/search/tables.py
@@ -0,0 +1,123 @@
+# 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:
+#
+# 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.
+
+"""The search data tables.
+"""
+
+
+import pandas as pd
+
+from ..utils.constants import Constants as C
+
+
+def select_search_data(data: pd.DataFrame, selection: list) -> pd.DataFrame:
+ """Return the searched data based on the user's "selection".
+
+ :param data: Input data to be searched through.
+ :param selection: User selection.
+ :type data: pandas.DataFrame
+ :type selection: list[dict]
+ :returns: A dataframe with selected tests.
+ :trype: pandas.DataFrame
+ """
+
+ sel_data = data[selection["datatype"]]
+
+ if selection["datatype"] == "trending":
+ df = pd.DataFrame(sel_data.loc[
+ sel_data["dut_type"] == selection["dut"]
+ ])
+ else:
+ df = pd.DataFrame(sel_data.loc[(
+ (sel_data["dut_type"] == selection["dut"]) &
+ (sel_data["release"] == selection["release"])
+ )])
+ try:
+ df = df[
+ df.full_id.str.contains(
+ selection["regexp"].replace(" ", ".*"),
+ regex=True
+ )
+ ]
+ except Exception:
+ return pd.DataFrame()
+
+ return df
+
+
+def search_table(data: pd.DataFrame, selection: list) -> pd.DataFrame:
+ """Generate a table listing tests based on user's selection.
+
+ :param data: Input data (all tests).
+ :param selection: User selection.
+ :type data: pandas.DataFrame
+ :type selection: list[dict]
+ :returns: A dataframe with selected tests/
+ :rtype: pandas.DataFrame
+ """
+
+ sel = select_search_data(data, selection)
+ if sel.empty:
+ return pd.DataFrame()
+
+ l_tb, l_nic, l_drv, l_test, = list(), list(), list(), list()
+ if selection["datatype"] == "trending":
+ cols = ["job", "test_id"]
+ else:
+ l_dutver = list()
+ cols = ["job", "test_id", "dut_version"]
+ for _, row in sel[cols].drop_duplicates().iterrows():
+ l_id = row["test_id"].split(".")
+ suite = l_id[-2].replace("2n1l-", "").replace("1n1l-", "").\
+ replace("2n-", "")
+ l_tb.append("-".join(row["job"].split("-")[-2:]))
+ l_nic.append(suite.split("-")[0])
+ if selection["datatype"] != "trending":
+ l_dutver.append(row["dut_version"])
+ for driver in C.DRIVERS:
+ if driver in suite:
+ l_drv.append(driver)
+ break
+ else:
+ l_drv.append("dpdk")
+ l_test.append(l_id[-1])
+
+ if selection["datatype"] == "trending":
+ selected = pd.DataFrame.from_dict({
+ "Test Bed": l_tb,
+ "NIC": l_nic,
+ "Driver": l_drv,
+ "Test": l_test
+ })
+
+ selected.sort_values(
+ by=["Test Bed", "NIC", "Driver", "Test"],
+ ascending=True,
+ inplace=True
+ )
+ else:
+ selected = pd.DataFrame.from_dict({
+ "DUT Version": l_dutver,
+ "Test Bed": l_tb,
+ "NIC": l_nic,
+ "Driver": l_drv,
+ "Test": l_test
+ })
+
+ selected.sort_values(
+ by=["DUT Version", "Test Bed", "NIC", "Driver", "Test"],
+ ascending=True,
+ inplace=True
+ )
+
+ return selected
diff --git a/csit.infra.dash/app/cdash/stats/__init__.py b/csit.infra.dash/app/cdash/stats/__init__.py
index f0d52c25b6..c6a5f639fe 100644
--- a/csit.infra.dash/app/cdash/stats/__init__.py
+++ b/csit.infra.dash/app/cdash/stats/__init__.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:
diff --git a/csit.infra.dash/app/cdash/stats/graphs.py b/csit.infra.dash/app/cdash/stats/graphs.py
index 2223848166..4b25396680 100644
--- a/csit.infra.dash/app/cdash/stats/graphs.py
+++ b/csit.infra.dash/app/cdash/stats/graphs.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:
diff --git a/csit.infra.dash/app/cdash/stats/layout.py b/csit.infra.dash/app/cdash/stats/layout.py
index 753eb37670..56b24e045a 100644
--- a/csit.infra.dash/app/cdash/stats/layout.py
+++ b/csit.infra.dash/app/cdash/stats/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:
@@ -29,7 +29,7 @@ from yaml import load, FullLoader, YAMLError
from ..utils.constants import Constants as C
from ..utils.control_panel import ControlPanel
from ..utils.utils import show_tooltip, gen_new_url, get_ttypes, get_cadences, \
- get_test_beds, get_job, generate_options, set_job_params
+ get_test_beds, get_job, generate_options, set_job_params, navbar_trending
from ..utils.url_processing import url_decode
from .graphs import graph_statistics, select_data
@@ -233,9 +233,7 @@ class Layout:
dbc.Row(
id="row-navbar",
class_name="g-0",
- children=[
- self._add_navbar()
- ]
+ children=[navbar_trending((False, False, True, False))]
),
dbc.Spinner(
dbc.Offcanvas(
@@ -284,43 +282,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.TREND_TITLE,
- external_link=True,
- href="/trending"
- )),
- dbc.NavItem(dbc.NavLink(
- C.NEWS_TITLE,
- external_link=True,
- href="/news"
- )),
- dbc.NavItem(dbc.NavLink(
- C.STATS_TITLE,
- active=True,
- external_link=True,
- href="/stats"
- )),
- dbc.NavItem(dbc.NavLink(
- "Documentation",
- id="btn-documentation",
- ))
- ],
- 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.
diff --git a/csit.infra.dash/app/cdash/stats/stats.py b/csit.infra.dash/app/cdash/stats/stats.py
index fdeef8b2f7..0217a6edb6 100644
--- a/csit.infra.dash/app/cdash/stats/stats.py
+++ b/csit.infra.dash/app/cdash/stats/stats.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:
diff --git a/csit.infra.dash/app/cdash/templates/base_layout.jinja2 b/csit.infra.dash/app/cdash/templates/base_layout.jinja2
index 72504804d6..7b0dadc5a0 100644
--- a/csit.infra.dash/app/cdash/templates/base_layout.jinja2
+++ b/csit.infra.dash/app/cdash/templates/base_layout.jinja2
@@ -52,6 +52,11 @@
</a>
</p>
<p>
+ <a href="/search/" class="btn btn-primary fw-bold w-25">
+ {{ search_title }}
+ </a>
+ </p>
+ <p>
<a href="/cdocs/" class="btn btn-primary fw-bold w-25">
Documentation
</a>
diff --git a/csit.infra.dash/app/cdash/trending/__init__.py b/csit.infra.dash/app/cdash/trending/__init__.py
index f0d52c25b6..c6a5f639fe 100644
--- a/csit.infra.dash/app/cdash/trending/__init__.py
+++ b/csit.infra.dash/app/cdash/trending/__init__.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:
diff --git a/csit.infra.dash/app/cdash/trending/graphs.py b/csit.infra.dash/app/cdash/trending/graphs.py
index 57fc165cb3..ede3a06fd4 100644
--- a/csit.infra.dash/app/cdash/trending/graphs.py
+++ b/csit.infra.dash/app/cdash/trending/graphs.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:
@@ -71,7 +71,7 @@ def graph_trending(
data: pd.DataFrame,
sel: dict,
layout: dict,
- normalize: bool
+ normalize: bool=False
) -> tuple:
"""Generate the trending graph(s) - MRR, NDR, PDR and for PDR also Latences
(result_latency_forward_pdr_50_avg).
diff --git a/csit.infra.dash/app/cdash/trending/layout.py b/csit.infra.dash/app/cdash/trending/layout.py
index 66aa1d199a..f6f96d713e 100644
--- a/csit.infra.dash/app/cdash/trending/layout.py
+++ b/csit.infra.dash/app/cdash/trending/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:
@@ -34,7 +34,8 @@ from ..utils.control_panel import ControlPanel
from ..utils.trigger import Trigger
from ..utils.telemetry_data import TelemetryData
from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \
- generate_options, get_list_group_items, graph_hdrh_latency
+ generate_options, get_list_group_items, navbar_trending, \
+ show_trending_graph_data
from ..utils.url_processing import url_decode
from .graphs import graph_trending, select_trending_data, graph_tm_trending
@@ -244,9 +245,7 @@ class Layout:
dbc.Row(
id="row-navbar",
class_name="g-0",
- children=[
- self._add_navbar()
- ]
+ children=[navbar_trending((True, False, False, False))]
),
dbc.Row(
id="row-main",
@@ -290,43 +289,6 @@ class Layout:
id="div-main-error"
)
- 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(
- children=[
- dbc.NavItem(dbc.NavLink(
- C.TREND_TITLE,
- active=True,
- external_link=True,
- href="/trending"
- )),
- dbc.NavItem(dbc.NavLink(
- C.NEWS_TITLE,
- external_link=True,
- href="/news"
- )),
- dbc.NavItem(dbc.NavLink(
- C.STATS_TITLE,
- external_link=True,
- href="/stats"
- )),
- dbc.NavItem(dbc.NavLink(
- "Documentation",
- id="btn-documentation",
- ))
- ],
- id="navbarsimple-main",
- 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.
@@ -1692,91 +1654,11 @@ class Layout:
"""
trigger = Trigger(callback_context.triggered)
-
- try:
- if trigger.idx == "tput":
- idx = 0
- elif trigger.idx == "bandwidth":
- idx = 1
- elif trigger.idx == "lat":
- idx = 2
- else:
- raise PreventUpdate
- graph_data = graph_data[idx]["points"][0]
- except (IndexError, KeyError, ValueError, TypeError):
- raise PreventUpdate
-
- metadata = no_update
- graph = list()
-
- list_group_items = list()
- for itm in graph_data.get("text", None).split("<br>"):
- if not itm:
- continue
- lst_itm = itm.split(": ")
- if lst_itm[0] == "csit-ref":
- list_group_item = dbc.ListGroupItem([
- dbc.Badge(lst_itm[0]),
- html.A(
- lst_itm[1],
- href=f"{C.URL_JENKINS}{lst_itm[1]}",
- target="_blank"
- )
- ])
- else:
- list_group_item = dbc.ListGroupItem([
- dbc.Badge(lst_itm[0]),
- lst_itm[1]
- ])
- list_group_items.append(list_group_item)
-
- if trigger.idx == "tput":
- title = "Throughput"
- elif trigger.idx == "bandwidth":
- title = "Bandwidth"
- elif trigger.idx == "lat":
- title = "Latency"
- hdrh_data = graph_data.get("customdata", None)
- if hdrh_data:
- graph = [dbc.Card(
- class_name="gy-2 p-0",
- children=[
- dbc.CardHeader(hdrh_data.pop("name")),
- dbc.CardBody(
- dcc.Graph(
- id="hdrh-latency-graph",
- figure=graph_hdrh_latency(
- hdrh_data, self._graph_layout
- )
- )
- )
- ])
- ]
- else:
+ if not trigger.value:
raise PreventUpdate
-
- metadata = [
- dbc.Card(
- class_name="gy-2 p-0",
- children=[
- dbc.CardHeader(children=[
- dcc.Clipboard(
- target_id="tput-lat-metadata",
- title="Copy",
- style={"display": "inline-block"}
- ),
- title
- ]),
- dbc.CardBody(
- dbc.ListGroup(list_group_items, flush=True),
- id="tput-lat-metadata",
- class_name="p-0",
- )
- ]
- )
- ]
-
- return metadata, graph, True
+
+ return show_trending_graph_data(
+ trigger, graph_data, self._graph_layout)
@app.callback(
Output("download-trending-data", "data"),
diff --git a/csit.infra.dash/app/cdash/trending/trending.py b/csit.infra.dash/app/cdash/trending/trending.py
index a9dfbc1987..257e3de625 100644
--- a/csit.infra.dash/app/cdash/trending/trending.py
+++ b/csit.infra.dash/app/cdash/trending/trending.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:
diff --git a/csit.infra.dash/app/cdash/utils/__init__.py b/csit.infra.dash/app/cdash/utils/__init__.py
index f0d52c25b6..c6a5f639fe 100644
--- a/csit.infra.dash/app/cdash/utils/__init__.py
+++ b/csit.infra.dash/app/cdash/utils/__init__.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:
diff --git a/csit.infra.dash/app/cdash/utils/anomalies.py b/csit.infra.dash/app/cdash/utils/anomalies.py
index 9a7b232fda..3deece2e04 100644
--- a/csit.infra.dash/app/cdash/utils/anomalies.py
+++ b/csit.infra.dash/app/cdash/utils/anomalies.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:
diff --git a/csit.infra.dash/app/cdash/utils/constants.py b/csit.infra.dash/app/cdash/utils/constants.py
index 4ffd7c1d52..c86f4d5136 100644
--- a/csit.infra.dash/app/cdash/utils/constants.py
+++ b/csit.infra.dash/app/cdash/utils/constants.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:
@@ -97,6 +97,12 @@ class Constants:
# The element is enabled and visible.
STYLE_ENABLED = {"visibility": "visible"}
+ # The element is not displayed.
+ STYLE_DONT_DISPLAY = {"display": "none"}
+
+ # The element is displaed.
+ STYLE_DISPLAY = {"display": "flex"}
+
# Checklist "All" is disabled.
CL_ALL_DISABLED = [
{
@@ -403,3 +409,18 @@ class Constants:
COVERAGE_DOWNLOAD_FILE_NAME = "coverage_data.csv"
############################################################################
+ # Search tests.
+
+ # The title.
+ SEARCH_TITLE = "Search Tests"
+
+ # The pathname prefix for the application.
+ SEARCH_ROUTES_PATHNAME_PREFIX = "/search/"
+
+ # Layout of plot.ly graphs.
+ SEARCH_GRAPH_LAYOUT_FILE = "cdash/search/layout.yaml"
+
+ # Default name of downloaded file with selected data.
+ SEARCH_DOWNLOAD_FILE_NAME = "search_data.csv"
+
+ ############################################################################
diff --git a/csit.infra.dash/app/cdash/utils/control_panel.py b/csit.infra.dash/app/cdash/utils/control_panel.py
index a81495e30c..3da44e3901 100644
--- a/csit.infra.dash/app/cdash/utils/control_panel.py
+++ b/csit.infra.dash/app/cdash/utils/control_panel.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:
diff --git a/csit.infra.dash/app/cdash/utils/telemetry_data.py b/csit.infra.dash/app/cdash/utils/telemetry_data.py
index 80187967fa..9975874d96 100644
--- a/csit.infra.dash/app/cdash/utils/telemetry_data.py
+++ b/csit.infra.dash/app/cdash/utils/telemetry_data.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:
diff --git a/csit.infra.dash/app/cdash/utils/trigger.py b/csit.infra.dash/app/cdash/utils/trigger.py
index ac303b6b0b..da0768b070 100644
--- a/csit.infra.dash/app/cdash/utils/trigger.py
+++ b/csit.infra.dash/app/cdash/utils/trigger.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:
diff --git a/csit.infra.dash/app/cdash/utils/url_processing.py b/csit.infra.dash/app/cdash/utils/url_processing.py
index c90c54c41f..c436ebc830 100644
--- a/csit.infra.dash/app/cdash/utils/url_processing.py
+++ b/csit.infra.dash/app/cdash/utils/url_processing.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:
diff --git a/csit.infra.dash/app/cdash/utils/utils.py b/csit.infra.dash/app/cdash/utils/utils.py
index 29bee3d039..3d2866fbe0 100644
--- a/csit.infra.dash/app/cdash/utils/utils.py
+++ b/csit.infra.dash/app/cdash/utils/utils.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:
@@ -22,11 +22,12 @@ import hdrh.histogram
import hdrh.codec
from math import sqrt
-from dash import dcc
+from dash import dcc, no_update, html
from datetime import datetime
from ..utils.constants import Constants as C
from ..utils.url_processing import url_encode
+from ..utils.trigger import Trigger
def get_color(idx: int) -> str:
@@ -468,3 +469,394 @@ def graph_hdrh_latency(data: dict, layout: dict) -> go.Figure:
fig.update_layout(layout_hdrh)
return fig
+
+
+def navbar_trending(active: tuple):
+ """Add nav element with navigation panel. It is placed on the top.
+
+ :param active: Tuple of boolean values defining the active items in the
+ navbar. True == active
+ :type active: tuple
+ :returns: Navigation bar.
+ :rtype: dbc.NavbarSimple
+ """
+ return dbc.NavbarSimple(
+ children=[
+ dbc.NavItem(dbc.NavLink(
+ C.TREND_TITLE,
+ active=active[0],
+ external_link=True,
+ href="/trending"
+ )),
+ dbc.NavItem(dbc.NavLink(
+ C.NEWS_TITLE,
+ active=active[1],
+ external_link=True,
+ href="/news"
+ )),
+ dbc.NavItem(dbc.NavLink(
+ C.STATS_TITLE,
+ active=active[2],
+ external_link=True,
+ href="/stats"
+ )),
+ dbc.NavItem(dbc.NavLink(
+ C.SEARCH_TITLE,
+ active=active[3],
+ external_link=True,
+ href="/search"
+ )),
+ dbc.NavItem(dbc.NavLink(
+ "Documentation",
+ id="btn-documentation",
+ ))
+ ],
+ id="navbarsimple-main",
+ brand=C.BRAND,
+ brand_href="/",
+ brand_external_link=True,
+ class_name="p-2",
+ fluid=True
+ )
+
+
+def navbar_report(active: tuple):
+ """Add nav element with navigation panel. It is placed on the top.
+
+ :param active: Tuple of boolean values defining the active items in the
+ navbar. True == active
+ :type active: tuple
+ :returns: Navigation bar.
+ :rtype: dbc.NavbarSimple
+ """
+ return dbc.NavbarSimple(
+ id="navbarsimple-main",
+ children=[
+ dbc.NavItem(dbc.NavLink(
+ C.REPORT_TITLE,
+ active=active[0],
+ external_link=True,
+ href="/report"
+ )),
+ dbc.NavItem(dbc.NavLink(
+ "Comparisons",
+ active=active[1],
+ external_link=True,
+ href="/comparisons"
+ )),
+ dbc.NavItem(dbc.NavLink(
+ "Coverage Data",
+ active=active[2],
+ external_link=True,
+ href="/coverage"
+ )),
+ dbc.NavItem(dbc.NavLink(
+ C.SEARCH_TITLE,
+ active=active[3],
+ external_link=True,
+ href="/search"
+ )),
+ dbc.NavItem(dbc.NavLink(
+ "Documentation",
+ id="btn-documentation",
+ ))
+ ],
+ brand=C.BRAND,
+ brand_href="/",
+ brand_external_link=True,
+ class_name="p-2",
+ fluid=True
+ )
+
+
+def filter_table_data(
+ store_table_data: list,
+ table_filter: str
+ ) -> list:
+ """Filter table data using user specified filter.
+
+ :param store_table_data: Table data represented as a list of records.
+ :param table_filter: User specified filter.
+ :type store_table_data: list
+ :type table_filter: str
+ :returns: A new table created by filtering of table data represented as
+ a list of records.
+ :rtype: list
+ """
+
+ # Checks:
+ if not any((table_filter, store_table_data, )):
+ return store_table_data
+
+ def _split_filter_part(filter_part: str) -> tuple:
+ """Split a part of filter into column name, operator and value.
+ A "part of filter" is a sting berween "&&" operator.
+
+ :param filter_part: A part of filter.
+ :type filter_part: str
+ :returns: Column name, operator, value
+ :rtype: tuple[str, str, str|float]
+ """
+ for operator_type in C.OPERATORS:
+ for operator in operator_type:
+ if operator in filter_part:
+ name_p, val_p = filter_part.split(operator, 1)
+ name = name_p[name_p.find("{") + 1 : name_p.rfind("}")]
+ val_p = val_p.strip()
+ if (val_p[0] == val_p[-1] and val_p[0] in ("'", '"', '`')):
+ value = val_p[1:-1].replace("\\" + val_p[0], val_p[0])
+ else:
+ try:
+ value = float(val_p)
+ except ValueError:
+ value = val_p
+
+ return name, operator_type[0].strip(), value
+ return (None, None, None)
+
+ df = pd.DataFrame.from_records(store_table_data)
+ for filter_part in table_filter.split(" && "):
+ col_name, operator, filter_value = _split_filter_part(filter_part)
+ if operator == "contains":
+ df = df.loc[df[col_name].str.contains(filter_value, regex=True)]
+ elif operator in ("eq", "ne", "lt", "le", "gt", "ge"):
+ # These operators match pandas series operator method names.
+ df = df.loc[getattr(df[col_name], operator)(filter_value)]
+ elif operator == "datestartswith":
+ # This is a simplification of the front-end filtering logic,
+ # only works with complete fields in standard format.
+ # Currently not used in comparison tables.
+ df = df.loc[df[col_name].str.startswith(filter_value)]
+
+ return df.to_dict("records")
+
+
+def show_trending_graph_data(
+ trigger: Trigger,
+ data: dict,
+ graph_layout: dict
+ ) -> tuple:
+ """Generates the data for the offcanvas displayed when a particular point in
+ a trending graph (daily data) is clicked on.
+
+ :param trigger: The information from trigger when the data point is clicked
+ on.
+ :param graph: The data from the clicked point in the graph.
+ :param graph_layout: The layout of the HDRH latency graph.
+ :type trigger: Trigger
+ :type graph: dict
+ :type graph_layout: dict
+ :returns: The data to be displayed on the offcanvas and the information to
+ show the offcanvas.
+ :rtype: tuple(list, list, bool)
+ """
+
+ if trigger.idx == "tput":
+ idx = 0
+ elif trigger.idx == "bandwidth":
+ idx = 1
+ elif trigger.idx == "lat":
+ idx = len(data) - 1
+ else:
+ return list(), list(), False
+ try:
+ data = data[idx]["points"][0]
+ except (IndexError, KeyError, ValueError, TypeError):
+ return list(), list(), False
+
+ metadata = no_update
+ graph = list()
+
+ list_group_items = list()
+ for itm in data.get("text", None).split("<br>"):
+ if not itm:
+ continue
+ lst_itm = itm.split(": ")
+ if lst_itm[0] == "csit-ref":
+ list_group_item = dbc.ListGroupItem([
+ dbc.Badge(lst_itm[0]),
+ html.A(
+ lst_itm[1],
+ href=f"{C.URL_JENKINS}{lst_itm[1]}",
+ target="_blank"
+ )
+ ])
+ else:
+ list_group_item = dbc.ListGroupItem([
+ dbc.Badge(lst_itm[0]),
+ lst_itm[1]
+ ])
+ list_group_items.append(list_group_item)
+
+ if trigger.idx == "tput":
+ title = "Throughput"
+ elif trigger.idx == "bandwidth":
+ title = "Bandwidth"
+ elif trigger.idx == "lat":
+ title = "Latency"
+ hdrh_data = data.get("customdata", None)
+ if hdrh_data:
+ graph = [dbc.Card(
+ class_name="gy-2 p-0",
+ children=[
+ dbc.CardHeader(hdrh_data.pop("name")),
+ dbc.CardBody(
+ dcc.Graph(
+ id="hdrh-latency-graph",
+ figure=graph_hdrh_latency(hdrh_data, graph_layout)
+ )
+ )
+ ])
+ ]
+
+ metadata = [
+ dbc.Card(
+ class_name="gy-2 p-0",
+ children=[
+ dbc.CardHeader(children=[
+ dcc.Clipboard(
+ target_id="tput-lat-metadata",
+ title="Copy",
+ style={"display": "inline-block"}
+ ),
+ title
+ ]),
+ dbc.CardBody(
+ dbc.ListGroup(list_group_items, flush=True),
+ id="tput-lat-metadata",
+ class_name="p-0",
+ )
+ ]
+ )
+ ]
+
+ return metadata, graph, True
+
+
+def show_iterative_graph_data(
+ trigger: Trigger,
+ data: dict,
+ graph_layout: dict
+ ) -> tuple:
+ """Generates the data for the offcanvas displayed when a particular point in
+ a box graph (iterative data) is clicked on.
+
+ :param trigger: The information from trigger when the data point is clicked
+ on.
+ :param graph: The data from the clicked point in the graph.
+ :param graph_layout: The layout of the HDRH latency graph.
+ :type trigger: Trigger
+ :type graph: dict
+ :type graph_layout: dict
+ :returns: The data to be displayed on the offcanvas and the information to
+ show the offcanvas.
+ :rtype: tuple(list, list, bool)
+ """
+
+ if trigger.idx == "tput":
+ idx = 0
+ elif trigger.idx == "bandwidth":
+ idx = 1
+ elif trigger.idx == "lat":
+ idx = len(data) - 1
+ else:
+ return list(), list(), False
+
+ try:
+ data = data[idx]["points"]
+ except (IndexError, KeyError, ValueError, TypeError):
+ return list(), list(), False
+
+ def _process_stats(data: list, param: str) -> list:
+ """Process statistical data provided by plot.ly box graph.
+
+ :param data: Statistical data provided by plot.ly box graph.
+ :param param: Parameter saying if the data come from "tput" or
+ "lat" graph.
+ :type data: list
+ :type param: str
+ :returns: Listo of tuples where the first value is the
+ statistic's name and the secont one it's value.
+ :rtype: list
+ """
+ if len(data) == 7:
+ stats = ("max", "upper fence", "q3", "median", "q1",
+ "lower fence", "min")
+ elif len(data) == 9:
+ stats = ("outlier", "max", "upper fence", "q3", "median",
+ "q1", "lower fence", "min", "outlier")
+ elif len(data) == 1:
+ if param == "lat":
+ stats = ("average latency at 50% PDR", )
+ elif param == "bandwidth":
+ stats = ("bandwidth", )
+ else:
+ stats = ("throughput", )
+ else:
+ return list()
+ unit = " [us]" if param == "lat" else str()
+ return [(f"{stat}{unit}", f"{value['y']:,.0f}")
+ for stat, value in zip(stats, data)]
+
+ customdata = data[0].get("customdata", dict())
+ datapoint = customdata.get("metadata", dict())
+ hdrh_data = customdata.get("hdrh", dict())
+
+ list_group_items = list()
+ for k, v in datapoint.items():
+ if k == "csit-ref":
+ if len(data) > 1:
+ continue
+ list_group_item = dbc.ListGroupItem([
+ dbc.Badge(k),
+ html.A(v, href=f"{C.URL_JENKINS}{v}", target="_blank")
+ ])
+ else:
+ list_group_item = dbc.ListGroupItem([dbc.Badge(k), v])
+ list_group_items.append(list_group_item)
+
+ graph = list()
+ if trigger.idx == "tput":
+ title = "Throughput"
+ elif trigger.idx == "bandwidth":
+ title = "Bandwidth"
+ elif trigger.idx == "lat":
+ title = "Latency"
+ if len(data) == 1:
+ if hdrh_data:
+ graph = [dbc.Card(
+ class_name="gy-2 p-0",
+ children=[
+ dbc.CardHeader(hdrh_data.pop("name")),
+ dbc.CardBody(dcc.Graph(
+ id="hdrh-latency-graph",
+ figure=graph_hdrh_latency(hdrh_data, graph_layout)
+ ))
+ ])
+ ]
+
+ for k, v in _process_stats(data, trigger.idx):
+ list_group_items.append(dbc.ListGroupItem([dbc.Badge(k), v]))
+
+ metadata = [
+ dbc.Card(
+ class_name="gy-2 p-0",
+ children=[
+ dbc.CardHeader(children=[
+ dcc.Clipboard(
+ target_id="tput-lat-metadata",
+ title="Copy",
+ style={"display": "inline-block"}
+ ),
+ title
+ ]),
+ dbc.CardBody(
+ dbc.ListGroup(list_group_items, flush=True),
+ id="tput-lat-metadata",
+ class_name="p-0"
+ )
+ ]
+ )
+ ]
+
+ return metadata, graph, True
diff --git a/csit.infra.dash/app/config.py b/csit.infra.dash/app/config.py
index 0d1d6fc007..a05379bce2 100644
--- a/csit.infra.dash/app/config.py
+++ b/csit.infra.dash/app/config.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-# 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:
diff --git a/csit.infra.dash/app/wsgi.py b/csit.infra.dash/app/wsgi.py
index f907d4ae00..16e094b6a4 100644
--- a/csit.infra.dash/app/wsgi.py
+++ b/csit.infra.dash/app/wsgi.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: