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
485 children.append(dbc.NavItem(dbc.NavLink(
492 children.append(dbc.NavItem(dbc.NavLink(
498 if C.START_STATISTICS:
499 children.append(dbc.NavItem(dbc.NavLink(
506 children.append(dbc.NavItem(dbc.NavLink(
513 children.append(dbc.NavItem(dbc.NavLink(
515 id="btn-documentation",
517 return dbc.NavbarSimple(
519 id="navbarsimple-main",
522 brand_external_link=True,
528 def navbar_report(active: tuple):
529 """Add nav element with navigation panel. It is placed on the top.
531 :param active: Tuple of boolean values defining the active items in the
532 navbar. True == active
534 :returns: Navigation bar.
535 :rtype: dbc.NavbarSimple
539 children.append(dbc.NavItem(dbc.NavLink(
545 if C.START_COMPARISONS:
546 children.append(dbc.NavItem(dbc.NavLink(
553 children.append(dbc.NavItem(dbc.NavLink(
560 children.append(dbc.NavItem(dbc.NavLink(
567 children.append(dbc.NavItem(dbc.NavLink(
569 id="btn-documentation",
571 return dbc.NavbarSimple(
573 id="navbarsimple-main",
576 brand_external_link=True,
582 def filter_table_data(
583 store_table_data: list,
586 """Filter table data using user specified filter.
588 :param store_table_data: Table data represented as a list of records.
589 :param table_filter: User specified filter.
590 :type store_table_data: list
591 :type table_filter: str
592 :returns: A new table created by filtering of table data represented as
598 if not any((table_filter, store_table_data, )):
599 return store_table_data
601 def _split_filter_part(filter_part: str) -> tuple:
602 """Split a part of filter into column name, operator and value.
603 A "part of filter" is a sting berween "&&" operator.
605 :param filter_part: A part of filter.
606 :type filter_part: str
607 :returns: Column name, operator, value
608 :rtype: tuple[str, str, str|float]
610 for operator_type in C.OPERATORS:
611 for operator in operator_type:
612 if operator in filter_part:
613 name_p, val_p = filter_part.split(operator, 1)
614 name = name_p[name_p.find("{") + 1 : name_p.rfind("}")]
615 val_p = val_p.strip()
616 if (val_p[0] == val_p[-1] and val_p[0] in ("'", '"', '`')):
617 value = val_p[1:-1].replace("\\" + val_p[0], val_p[0])
624 return name, operator_type[0].strip(), value
625 return (None, None, None)
627 df = pd.DataFrame.from_records(store_table_data)
628 for filter_part in table_filter.split(" && "):
629 col_name, operator, filter_value = _split_filter_part(filter_part)
630 if operator == "contains":
631 df = df.loc[df[col_name].str.contains(filter_value, regex=True)]
632 elif operator in ("eq", "ne", "lt", "le", "gt", "ge"):
633 # These operators match pandas series operator method names.
634 df = df.loc[getattr(df[col_name], operator)(filter_value)]
635 elif operator == "datestartswith":
636 # This is a simplification of the front-end filtering logic,
637 # only works with complete fields in standard format.
638 # Currently not used in comparison tables.
639 df = df.loc[df[col_name].str.startswith(filter_value)]
641 return df.to_dict("records")
645 store_table_data: list,
648 """Sort table data using user specified order.
650 :param store_table_data: Table data represented as a list of records.
651 :param sort_by: User specified sorting order (multicolumn).
652 :type store_table_data: list
654 :returns: A new table created by sorting the table data represented as
660 if not any((sort_by, store_table_data, )):
661 return store_table_data
663 df = pd.DataFrame.from_records(store_table_data)
665 dff = df.sort_values(
666 [col["column_id"] for col in sort_by],
667 ascending=[col["direction"] == "asc" for col in sort_by],
674 return dff.to_dict("records")
677 def show_trending_graph_data(
682 """Generates the data for the offcanvas displayed when a particular point in
683 a trending graph (daily data) is clicked on.
685 :param trigger: The information from trigger when the data point is clicked
687 :param graph: The data from the clicked point in the graph.
688 :param graph_layout: The layout of the HDRH latency graph.
689 :type trigger: Trigger
691 :type graph_layout: dict
692 :returns: The data to be displayed on the offcanvas and the information to
694 :rtype: tuple(list, list, bool)
697 if trigger.idx == "tput":
699 elif trigger.idx == "bandwidth":
701 elif trigger.idx == "lat":
704 return list(), list(), False
706 data = data[idx]["points"][0]
707 except (IndexError, KeyError, ValueError, TypeError):
708 return list(), list(), False
713 list_group_items = list()
714 for itm in data.get("text", None).split("<br>"):
717 lst_itm = itm.split(": ")
718 if lst_itm[0] == "csit-ref":
719 list_group_item = dbc.ListGroupItem([
720 dbc.Badge(lst_itm[0]),
723 href=f"{C.URL_LOGS}{lst_itm[1]}",
728 list_group_item = dbc.ListGroupItem([
729 dbc.Badge(lst_itm[0]),
732 list_group_items.append(list_group_item)
734 if trigger.idx == "tput":
736 elif trigger.idx == "bandwidth":
738 elif trigger.idx == "lat":
740 hdrh_data = data.get("customdata", None)
743 class_name="gy-2 p-0",
745 dbc.CardHeader(hdrh_data.pop("name")),
748 id="hdrh-latency-graph",
749 figure=graph_hdrh_latency(hdrh_data, graph_layout)
757 class_name="gy-2 p-0",
759 dbc.CardHeader(children=[
761 target_id="tput-lat-metadata",
763 style={"display": "inline-block"}
768 dbc.ListGroup(list_group_items, flush=True),
769 id="tput-lat-metadata",
776 return metadata, graph, True
779 def show_iterative_graph_data(
784 """Generates the data for the offcanvas displayed when a particular point in
785 a box graph (iterative data) is clicked on.
787 :param trigger: The information from trigger when the data point is clicked
789 :param graph: The data from the clicked point in the graph.
790 :param graph_layout: The layout of the HDRH latency graph.
791 :type trigger: Trigger
793 :type graph_layout: dict
794 :returns: The data to be displayed on the offcanvas and the information to
796 :rtype: tuple(list, list, bool)
799 if trigger.idx == "tput":
801 elif trigger.idx == "bandwidth":
803 elif trigger.idx == "lat":
806 return list(), list(), False
809 data = data[idx]["points"]
810 except (IndexError, KeyError, ValueError, TypeError):
811 return list(), list(), False
813 def _process_stats(data: list, param: str) -> list:
814 """Process statistical data provided by plot.ly box graph.
816 :param data: Statistical data provided by plot.ly box graph.
817 :param param: Parameter saying if the data come from "tput" or
821 :returns: Listo of tuples where the first value is the
822 statistic's name and the secont one it's value.
826 stats = ("max", "upper fence", "q3", "median", "q1",
827 "lower fence", "min")
829 stats = ("outlier", "max", "upper fence", "q3", "median",
830 "q1", "lower fence", "min", "outlier")
833 stats = ("average latency at 50% PDR", )
834 elif param == "bandwidth":
835 stats = ("bandwidth", )
837 stats = ("throughput", )
840 unit = " [us]" if param == "lat" else str()
841 return [(f"{stat}{unit}", f"{value['y']:,.0f}")
842 for stat, value in zip(stats, data)]
844 customdata = data[0].get("customdata", dict())
845 datapoint = customdata.get("metadata", dict())
846 hdrh_data = customdata.get("hdrh", dict())
848 list_group_items = list()
849 for k, v in datapoint.items():
853 list_group_item = dbc.ListGroupItem([
855 html.A(v, href=f"{C.URL_LOGS}{v}", target="_blank")
858 list_group_item = dbc.ListGroupItem([dbc.Badge(k), v])
859 list_group_items.append(list_group_item)
862 if trigger.idx == "tput":
864 elif trigger.idx == "bandwidth":
866 elif trigger.idx == "lat":
871 class_name="gy-2 p-0",
873 dbc.CardHeader(hdrh_data.pop("name")),
874 dbc.CardBody(dcc.Graph(
875 id="hdrh-latency-graph",
876 figure=graph_hdrh_latency(hdrh_data, graph_layout)
881 for k, v in _process_stats(data, trigger.idx):
882 list_group_items.append(dbc.ListGroupItem([dbc.Badge(k), v]))
886 class_name="gy-2 p-0",
888 dbc.CardHeader(children=[
890 target_id="tput-lat-metadata",
892 style={"display": "inline-block"}
897 dbc.ListGroup(list_group_items, flush=True),
898 id="tput-lat-metadata",
905 return metadata, graph, True