1 # Copyright (c) 2024 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 """Functions used by Dash applications.
18 import plotly.graph_objects as go
19 import dash_bootstrap_components as dbc
25 from dash import dcc, no_update, html
26 from datetime import datetime
28 from ..utils.constants import Constants as C
29 from ..utils.url_processing import url_encode
30 from ..utils.trigger import Trigger
33 def get_color(idx: int) -> str:
34 """Returns a color from the list defined in Constants.PLOT_COLORS defined by
37 :param idx: Index of the color.
39 :returns: Color defined by hex code.
42 return C.PLOT_COLORS[idx % len(C.PLOT_COLORS)]
45 def show_tooltip(tooltips:dict, id: str, title: str,
46 clipboard_id: str=None) -> list:
47 """Generate list of elements to display a text (e.g. a title) with a
48 tooltip and optionaly with Copy&Paste icon and the clipboard
49 functionality enabled.
51 :param tooltips: Dictionary with tooltips.
52 :param id: Tooltip ID.
53 :param title: A text for which the tooltip will be displayed.
54 :param clipboard_id: If defined, a Copy&Paste icon is displayed and the
55 clipboard functionality is enabled.
59 :type clipboard_id: str
60 :returns: List of elements to display a text with a tooltip and
61 optionaly with Copy&Paste icon.
66 dcc.Clipboard(target_id=clipboard_id, title="Copy URL") \
67 if clipboard_id else str(),
75 class_name="border ms-1",
78 children=tooltips.get(id, str()),
85 def label(key: str) -> str:
86 """Returns a label for input elements (dropdowns, ...).
88 If the label is not defined, the function returns the provided key.
90 :param key: The key to the label defined in Constants.LABELS.
95 return C.LABELS.get(key, key)
98 def sync_checklists(options: list, sel: list, all: list, id: str) -> tuple:
99 """Synchronize a checklist with defined "options" with its "All" checklist.
101 :param options: List of options for the cheklist.
102 :param sel: List of selected options.
103 :param all: List of selected option from "All" checklist.
104 :param id: ID of a checklist to be used for synchronization.
105 :returns: Tuple of lists with otions for both checklists.
106 :rtype: tuple of lists
108 opts = {v["value"] for v in options}
110 sel = list(opts) if all else list()
112 all = ["all", ] if set(sel) == opts else list()
116 def list_tests(selection: dict) -> list:
117 """Transform list of tests to a list of dictionaries usable by checkboxes.
119 :param selection: List of tests to be displayed in "Selected tests" window.
120 :type selection: list
121 :returns: List of dictionaries with "label", "value" pairs for a checkbox.
125 return [{"label": v["id"], "value": v["id"]} for v in selection]
130 def get_date(s_date: str) -> datetime:
131 """Transform string reprezentation of date to datetime.datetime data type.
133 :param s_date: String reprezentation of date.
135 :returns: Date as datetime.datetime.
136 :rtype: datetime.datetime
138 return datetime(int(s_date[0:4]), int(s_date[5:7]), int(s_date[8:10]))
141 def gen_new_url(url_components: dict, params: dict) -> str:
142 """Generate a new URL with encoded parameters.
144 :param url_components: Dictionary with URL elements. It should contain
145 "scheme", "netloc" and "path".
146 :param url_components: URL parameters to be encoded to the URL.
147 :type parsed_url: dict
149 :returns Encoded URL with parameters.
156 "scheme": url_components.get("scheme", ""),
157 "netloc": url_components.get("netloc", ""),
158 "path": url_components.get("path", ""),
166 def get_duts(df: pd.DataFrame) -> list:
167 """Get the list of DUTs from the pre-processed information about jobs.
169 :param df: DataFrame with information about jobs.
170 :type df: pandas.DataFrame
171 :returns: Alphabeticaly sorted list of DUTs.
174 return sorted(list(df["dut"].unique()))
177 def get_ttypes(df: pd.DataFrame, dut: str) -> list:
178 """Get the list of test types from the pre-processed information about
181 :param df: DataFrame with information about jobs.
182 :param dut: The DUT for which the list of test types will be populated.
183 :type df: pandas.DataFrame
185 :returns: Alphabeticaly sorted list of test types.
188 return sorted(list(df.loc[(df["dut"] == dut)]["ttype"].unique()))
191 def get_cadences(df: pd.DataFrame, dut: str, ttype: str) -> list:
192 """Get the list of cadences from the pre-processed information about
195 :param df: DataFrame with information about jobs.
196 :param dut: The DUT for which the list of cadences will be populated.
197 :param ttype: The test type for which the list of cadences will be
199 :type df: pandas.DataFrame
202 :returns: Alphabeticaly sorted list of cadences.
205 return sorted(list(df.loc[(
207 (df["ttype"] == ttype)
208 )]["cadence"].unique()))
211 def get_test_beds(df: pd.DataFrame, dut: str, ttype: str, cadence: str) -> list:
212 """Get the list of test beds from the pre-processed information about
215 :param df: DataFrame with information about jobs.
216 :param dut: The DUT for which the list of test beds will be populated.
217 :param ttype: The test type for which the list of test beds will be
219 :param cadence: The cadence for which the list of test beds will be
221 :type df: pandas.DataFrame
225 :returns: Alphabeticaly sorted list of test beds.
228 return sorted(list(df.loc[(
230 (df["ttype"] == ttype) &
231 (df["cadence"] == cadence)
232 )]["tbed"].unique()))
235 def get_job(df: pd.DataFrame, dut, ttype, cadence, testbed):
236 """Get the name of a job defined by dut, ttype, cadence, test bed.
237 Input information comes from the control panel.
239 :param df: DataFrame with information about jobs.
240 :param dut: The DUT for which the job name will be created.
241 :param ttype: The test type for which the job name will be created.
242 :param cadence: The cadence for which the job name will be created.
243 :param testbed: The test bed for which the job name will be created.
244 :type df: pandas.DataFrame
254 (df["ttype"] == ttype) &
255 (df["cadence"] == cadence) &
256 (df["tbed"] == testbed)
260 def generate_options(opts: list, sort: bool=True) -> list:
261 """Return list of options for radio items in control panel. The items in
262 the list are dictionaries with keys "label" and "value".
264 :params opts: List of options (str) to be used for the generated list.
266 :returns: List of options (dict).
271 return [{"label": i, "value": i} for i in opts]
274 def set_job_params(df: pd.DataFrame, job: str) -> dict:
275 """Create a dictionary with all options and values for (and from) the
278 :param df: DataFrame with information about jobs.
279 :params job: The name of job for and from which the dictionary will be
281 :type df: pandas.DataFrame
283 :returns: Dictionary with all options and values for (and from) the
288 l_job = job.split("-")
294 "tbed": "-".join(l_job[-2:]),
295 "duts": generate_options(get_duts(df)),
296 "ttypes": generate_options(get_ttypes(df, l_job[1])),
297 "cadences": generate_options(get_cadences(df, l_job[1], l_job[3])),
298 "tbeds": generate_options(
299 get_test_beds(df, l_job[1], l_job[3], l_job[4]))
303 def get_list_group_items(
307 add_index: bool=False
309 """Generate list of ListGroupItems with checkboxes with selected items.
311 :param items: List of items to be displayed in the ListGroup.
312 :param type: The type part of an element ID.
313 :param colorize: If True, the color of labels is set, otherwise the default
315 :param add_index: Add index to the list items.
319 :type add_index: bool
320 :returns: List of ListGroupItems with checkboxes with selected items.
325 for i, l in enumerate(items):
326 idx = f"{i + 1}. " if add_index else str()
327 label = f"{idx}{l['id']}" if isinstance(l, dict) else f"{idx}{l}"
332 id={"type": type, "index": i},
335 label_class_name="m-0 p-0",
337 "font-size": ".875em",
338 "color": get_color(i) if colorize else "#55595c"
350 def relative_change_stdev(mean1, mean2, std1, std2):
351 """Compute relative standard deviation of change of two values.
353 The "1" values are the base for comparison.
354 Results are returned as percentage (and percentual points for stdev).
355 Linearized theory is used, so results are wrong for relatively large stdev.
357 :param mean1: Mean of the first number.
358 :param mean2: Mean of the second number.
359 :param std1: Standard deviation estimate of the first number.
360 :param std2: Standard deviation estimate of the second number.
365 :returns: Relative change and its stdev.
368 mean1, mean2 = float(mean1), float(mean2)
369 quotient = mean2 / mean1
371 second = std2 / mean2
372 std = quotient * sqrt(first * first + second * second)
373 return (quotient - 1) * 100, std * 100
376 def get_hdrh_latencies(row: pd.Series, name: str) -> dict:
377 """Get the HDRH latencies from the test data.
379 :param row: A row fron the data frame with test data.
380 :param name: The test name to be displayed as the graph title.
381 :type row: pandas.Series
383 :returns: Dictionary with HDRH latencies.
387 latencies = {"name": name}
388 for key in C.LAT_HDRH:
390 latencies[key] = row[key]
397 def graph_hdrh_latency(data: dict, layout: dict) -> go.Figure:
398 """Generate HDR Latency histogram graphs.
400 :param data: HDRH data.
401 :param layout: Layout of plot.ly graph.
404 :returns: HDR latency Histogram.
405 :rtype: plotly.graph_objects.Figure
411 for idx, (lat_name, lat_hdrh) in enumerate(data.items()):
413 decoded = hdrh.histogram.HdrHistogram.decode(lat_hdrh)
414 except (hdrh.codec.HdrLengthException, TypeError):
421 for item in decoded.get_recorded_iterator():
422 # The real value is "percentile".
423 # For 100%, we cut that down to "x_perc" to avoid
425 percentile = item.percentile_level_iterated_to
426 x_perc = min(percentile, C.PERCENTILE_MAX)
427 xaxis.append(previous_x)
428 yaxis.append(item.value_iterated_to)
430 f"<b>{C.GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
431 f"Direction: {('W-E', 'E-W')[idx % 2]}<br>"
432 f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
433 f"Latency: {item.value_iterated_to}uSec"
435 next_x = 100.0 / (100.0 - x_perc)
437 yaxis.append(item.value_iterated_to)
439 f"<b>{C.GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
440 f"Direction: {('W-E', 'E-W')[idx % 2]}<br>"
441 f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
442 f"Latency: {item.value_iterated_to}uSec"
445 prev_perc = percentile
451 name=C.GRAPH_LAT_HDRH_DESC[lat_name],
453 legendgroup=C.GRAPH_LAT_HDRH_DESC[lat_name],
454 showlegend=bool(idx % 2),
456 color=get_color(int(idx/2)),
458 width=1 if idx % 2 else 2
466 fig.add_traces(traces)
467 layout_hdrh = layout.get("plot-hdrh-latency", None)
469 fig.update_layout(layout_hdrh)
474 def navbar_trending(active: tuple):
475 """Add nav element with navigation panel. It is placed on the top.
477 :param active: Tuple of boolean values defining the active items in the
478 navbar. True == active
480 :returns: Navigation bar.
481 :rtype: dbc.NavbarSimple
483 return dbc.NavbarSimple(
485 dbc.NavItem(dbc.NavLink(
491 dbc.NavItem(dbc.NavLink(
497 dbc.NavItem(dbc.NavLink(
503 dbc.NavItem(dbc.NavLink(
509 dbc.NavItem(dbc.NavLink(
511 id="btn-documentation",
514 id="navbarsimple-main",
517 brand_external_link=True,
523 def navbar_report(active: tuple):
524 """Add nav element with navigation panel. It is placed on the top.
526 :param active: Tuple of boolean values defining the active items in the
527 navbar. True == active
529 :returns: Navigation bar.
530 :rtype: dbc.NavbarSimple
532 return dbc.NavbarSimple(
533 id="navbarsimple-main",
535 dbc.NavItem(dbc.NavLink(
541 dbc.NavItem(dbc.NavLink(
547 dbc.NavItem(dbc.NavLink(
553 dbc.NavItem(dbc.NavLink(
559 dbc.NavItem(dbc.NavLink(
561 id="btn-documentation",
566 brand_external_link=True,
572 def filter_table_data(
573 store_table_data: list,
576 """Filter table data using user specified filter.
578 :param store_table_data: Table data represented as a list of records.
579 :param table_filter: User specified filter.
580 :type store_table_data: list
581 :type table_filter: str
582 :returns: A new table created by filtering of table data represented as
588 if not any((table_filter, store_table_data, )):
589 return store_table_data
591 def _split_filter_part(filter_part: str) -> tuple:
592 """Split a part of filter into column name, operator and value.
593 A "part of filter" is a sting berween "&&" operator.
595 :param filter_part: A part of filter.
596 :type filter_part: str
597 :returns: Column name, operator, value
598 :rtype: tuple[str, str, str|float]
600 for operator_type in C.OPERATORS:
601 for operator in operator_type:
602 if operator in filter_part:
603 name_p, val_p = filter_part.split(operator, 1)
604 name = name_p[name_p.find("{") + 1 : name_p.rfind("}")]
605 val_p = val_p.strip()
606 if (val_p[0] == val_p[-1] and val_p[0] in ("'", '"', '`')):
607 value = val_p[1:-1].replace("\\" + val_p[0], val_p[0])
614 return name, operator_type[0].strip(), value
615 return (None, None, None)
617 df = pd.DataFrame.from_records(store_table_data)
618 for filter_part in table_filter.split(" && "):
619 col_name, operator, filter_value = _split_filter_part(filter_part)
620 if operator == "contains":
621 df = df.loc[df[col_name].str.contains(filter_value, regex=True)]
622 elif operator in ("eq", "ne", "lt", "le", "gt", "ge"):
623 # These operators match pandas series operator method names.
624 df = df.loc[getattr(df[col_name], operator)(filter_value)]
625 elif operator == "datestartswith":
626 # This is a simplification of the front-end filtering logic,
627 # only works with complete fields in standard format.
628 # Currently not used in comparison tables.
629 df = df.loc[df[col_name].str.startswith(filter_value)]
631 return df.to_dict("records")
634 def show_trending_graph_data(
639 """Generates the data for the offcanvas displayed when a particular point in
640 a trending graph (daily data) is clicked on.
642 :param trigger: The information from trigger when the data point is clicked
644 :param graph: The data from the clicked point in the graph.
645 :param graph_layout: The layout of the HDRH latency graph.
646 :type trigger: Trigger
648 :type graph_layout: dict
649 :returns: The data to be displayed on the offcanvas and the information to
651 :rtype: tuple(list, list, bool)
654 if trigger.idx == "tput":
656 elif trigger.idx == "bandwidth":
658 elif trigger.idx == "lat":
661 return list(), list(), False
663 data = data[idx]["points"][0]
664 except (IndexError, KeyError, ValueError, TypeError):
665 return list(), list(), False
670 list_group_items = list()
671 for itm in data.get("text", None).split("<br>"):
674 lst_itm = itm.split(": ")
675 if lst_itm[0] == "csit-ref":
676 list_group_item = dbc.ListGroupItem([
677 dbc.Badge(lst_itm[0]),
680 href=f"{C.URL_JENKINS}{lst_itm[1]}",
685 list_group_item = dbc.ListGroupItem([
686 dbc.Badge(lst_itm[0]),
689 list_group_items.append(list_group_item)
691 if trigger.idx == "tput":
693 elif trigger.idx == "bandwidth":
695 elif trigger.idx == "lat":
697 hdrh_data = data.get("customdata", None)
700 class_name="gy-2 p-0",
702 dbc.CardHeader(hdrh_data.pop("name")),
705 id="hdrh-latency-graph",
706 figure=graph_hdrh_latency(hdrh_data, graph_layout)
714 class_name="gy-2 p-0",
716 dbc.CardHeader(children=[
718 target_id="tput-lat-metadata",
720 style={"display": "inline-block"}
725 dbc.ListGroup(list_group_items, flush=True),
726 id="tput-lat-metadata",
733 return metadata, graph, True
736 def show_iterative_graph_data(
741 """Generates the data for the offcanvas displayed when a particular point in
742 a box graph (iterative data) is clicked on.
744 :param trigger: The information from trigger when the data point is clicked
746 :param graph: The data from the clicked point in the graph.
747 :param graph_layout: The layout of the HDRH latency graph.
748 :type trigger: Trigger
750 :type graph_layout: dict
751 :returns: The data to be displayed on the offcanvas and the information to
753 :rtype: tuple(list, list, bool)
756 if trigger.idx == "tput":
758 elif trigger.idx == "bandwidth":
760 elif trigger.idx == "lat":
763 return list(), list(), False
766 data = data[idx]["points"]
767 except (IndexError, KeyError, ValueError, TypeError):
768 return list(), list(), False
770 def _process_stats(data: list, param: str) -> list:
771 """Process statistical data provided by plot.ly box graph.
773 :param data: Statistical data provided by plot.ly box graph.
774 :param param: Parameter saying if the data come from "tput" or
778 :returns: Listo of tuples where the first value is the
779 statistic's name and the secont one it's value.
783 stats = ("max", "upper fence", "q3", "median", "q1",
784 "lower fence", "min")
786 stats = ("outlier", "max", "upper fence", "q3", "median",
787 "q1", "lower fence", "min", "outlier")
790 stats = ("average latency at 50% PDR", )
791 elif param == "bandwidth":
792 stats = ("bandwidth", )
794 stats = ("throughput", )
797 unit = " [us]" if param == "lat" else str()
798 return [(f"{stat}{unit}", f"{value['y']:,.0f}")
799 for stat, value in zip(stats, data)]
801 customdata = data[0].get("customdata", dict())
802 datapoint = customdata.get("metadata", dict())
803 hdrh_data = customdata.get("hdrh", dict())
805 list_group_items = list()
806 for k, v in datapoint.items():
810 list_group_item = dbc.ListGroupItem([
812 html.A(v, href=f"{C.URL_JENKINS}{v}", target="_blank")
815 list_group_item = dbc.ListGroupItem([dbc.Badge(k), v])
816 list_group_items.append(list_group_item)
819 if trigger.idx == "tput":
821 elif trigger.idx == "bandwidth":
823 elif trigger.idx == "lat":
828 class_name="gy-2 p-0",
830 dbc.CardHeader(hdrh_data.pop("name")),
831 dbc.CardBody(dcc.Graph(
832 id="hdrh-latency-graph",
833 figure=graph_hdrh_latency(hdrh_data, graph_layout)
838 for k, v in _process_stats(data, trigger.idx):
839 list_group_items.append(dbc.ListGroupItem([dbc.Badge(k), v]))
843 class_name="gy-2 p-0",
845 dbc.CardHeader(children=[
847 target_id="tput-lat-metadata",
849 style={"display": "inline-block"}
854 dbc.ListGroup(list_group_items, flush=True),
855 id="tput-lat-metadata",
862 return metadata, graph, True