-# Copyright (c) 2022 Cisco and/or its affiliates.
+# Copyright (c) 2023 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:
"""Plotly Dash HTML layout override.
"""
-import logging
import pandas as pd
import dash_bootstrap_components as dbc
from dash import dcc
from dash import html
from dash import callback_context
-from dash import Input, Output
-from yaml import load, FullLoader, YAMLError
+from dash import Input, Output, State
-from ..data.data import Data
from ..utils.constants import Constants as C
-from ..utils.utils import classify_anomalies, show_tooltip, gen_new_url
+from ..utils.utils import gen_new_url
+from ..utils.anomalies import classify_anomalies
from ..utils.url_processing import url_decode
-from ..data.data import Data
from .tables import table_summary
"""The layout of the dash app and the callbacks.
"""
- def __init__(self, app: Flask, html_layout_file: str, data_spec_file: str,
- tooltip_file: str) -> None:
+ def __init__(
+ self,
+ app: Flask,
+ data_stats: pd.DataFrame,
+ data_trending: pd.DataFrame,
+ html_layout_file: str
+ ) -> None:
"""Initialization:
- save the input parameters,
- read and pre-process the data,
- read tooltips from the tooltip file.
:param app: Flask application running the dash application.
+ :param data_stats: Pandas dataframe with staistical data.
+ :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 data_spec_file: Path and name of the file specifying the data to
- be read from parquets for this application.
- :param tooltip_file: Path and name of the yaml file specifying the
- tooltips.
:type app: Flask
+ :type data_stats: pandas.DataFrame
+ :type data_trending: pandas.DataFrame
:type html_layout_file: str
- :type data_spec_file: str
- :type tooltip_file: str
"""
# Inputs
self._app = app
self._html_layout_file = html_layout_file
- self._data_spec_file = data_spec_file
- self._tooltip_file = tooltip_file
-
- # Read the data:
- data_stats, data_mrr, data_ndrpdr = Data(
- data_spec_file=self._data_spec_file,
- debug=True
- ).read_stats(days=C.NEWS_TIME_PERIOD)
-
- df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
# Prepare information for the control panel:
- self._jobs = sorted(list(df_tst_info["job"].unique()))
+ self._jobs = sorted(list(data_trending["job"].unique()))
d_job_info = {
"job": list(),
"dut": list(),
}
for job in self._jobs:
# Create lists of failed tests:
- df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
+ df_job = data_trending.loc[(data_trending["job"] == job)]
last_build = str(max(pd.to_numeric(df_job["build"].unique())))
df_build = df_job.loc[(df_job["build"] == last_build)]
tst_info["job"].append(job)
tests = df_job["test_id"].unique()
for test in tests:
- tst_data = df_job.loc[df_job["test_id"] == test].sort_values(
- by="start_time", ignore_index=True)
- x_axis = tst_data["start_time"].tolist()
+ tst_data = df_job.loc[(
+ (df_job["test_id"] == test) &
+ (df_job["passed"] == True)
+ )].sort_values(by="start_time", ignore_index=True)
if "-ndrpdr" in test:
tst_data = tst_data.dropna(
subset=["result_pdr_lower_rate_value", ]
)
if tst_data.empty:
continue
+ x_axis = tst_data["start_time"].tolist()
try:
anomalies, _, _ = classify_anomalies({
k: v for k, v in zip(
)
if tst_data.empty:
continue
+ x_axis = tst_data["start_time"].tolist()
try:
anomalies, _, _ = classify_anomalies({
k: v for k, v in zip(
# Read from files:
self._html_layout = str()
- self._tooltips = dict()
try:
with open(self._html_layout_file, "r") as file_read:
f"Not possible to open the file {self._html_layout_file}\n{err}"
)
- try:
- with open(self._tooltip_file, "r") as file_read:
- self._tooltips = load(file_read, Loader=FullLoader)
- except IOError as err:
- logging.warning(
- f"Not possible to open the file {self._tooltip_file}\n{err}"
- )
- except YAMLError as err:
- logging.warning(
- f"An error occurred while parsing the specification file "
- f"{self._tooltip_file}\n{err}"
- )
-
self._default_period = C.NEWS_SHORT
self._default_active = (False, True, False)
- self._default_table = \
- table_summary(self._data, self._jobs, self._default_period)
# Callbacks:
if self._app is not None and hasattr(self, 'callbacks'):
id="row-navbar",
class_name="g-0",
children=[
- self._add_navbar(),
+ self._add_navbar()
]
),
dbc.Row(
class_name="g-0",
children=[
self._add_ctrl_col(),
- self._add_plotting_col(),
+ self._add_plotting_col()
]
+ ),
+ dbc.Offcanvas(
+ class_name="w-75",
+ id="offcanvas-documentation",
+ title="Documentation",
+ placement="end",
+ is_open=False,
+ children=html.Iframe(
+ src=C.URL_DOC_TRENDING,
+ width="100%",
+ height="100%"
+ )
)
]
)
children=[
dbc.Alert(
[
- "An Error Occured",
+ "An Error Occured"
],
- color="danger",
- ),
+ color="danger"
+ )
]
)
return dbc.NavbarSimple(
id="navbarsimple-main",
children=[
- dbc.NavItem(
- dbc.NavLink(
- C.NEWS_TITLE,
- disabled=True,
- external_link=True,
- href="#"
- )
- )
+ 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,
+ fluid=True
)
def _add_ctrl_col(self) -> dbc.Col:
:returns: Column with tables.
:rtype: dbc.Col
"""
-
return dbc.Col(
id="col-plotting-area",
children=[
- dcc.Loading(
+ dbc.Spinner(
children=[
- dbc.Row( # Failed tests
- id="row-table",
- class_name="g-0 p-2",
- children=self._default_table
- ),
dbc.Row(
- class_name="g-0 p-2",
- align="center",
- justify="start",
+ id="plotting-area",
+ class_name="g-0 p-0",
children=[
- dbc.InputGroup(
- class_name="me-1",
- children=[
- dbc.InputGroupText(
- style=C.URL_STYLE,
- children=show_tooltip(
- self._tooltips,
- "help-url", "URL",
- "input-url"
- )
- ),
- dbc.Input(
- id="input-url",
- readonly=True,
- type="url",
- style=C.URL_STYLE,
- value=""
- )
- ]
- )
+ C.PLACEHOLDER
]
)
]
)
],
- width=9,
+ width=9
)
- def _add_ctrl_panel(self) -> dbc.Row:
+ def _add_ctrl_panel(self) -> list:
"""Add control panel.
:returns: Control panel.
- :rtype: dbc.Row
+ :rtype: list
"""
return [
- dbc.Label(
- class_name="g-0 p-1",
- children=show_tooltip(self._tooltips,
- "help-summary-period", "Window")
- ),
dbc.Row(
class_name="g-0 p-1",
children=[
)
]
+ def _get_plotting_area(
+ self,
+ period: int,
+ url: str
+ ) -> list:
+ """Generate the plotting area with all its content.
+
+ :param period: The time period for summary tables.
+ :param url: URL to be displayed in the modal window.
+ :type period: int
+ :type url: str
+ :returns: The content of the plotting area.
+ :rtype: list
+ """
+ return [
+ dbc.Row(
+ id="row-table",
+ class_name="g-0 p-1",
+ children=table_summary(self._data, self._jobs, period)
+ ),
+ 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
+ )
+ ],
+ className=\
+ "d-grid gap-0 d-md-flex justify-content-md-end"
+ )])
+ ],
+ class_name="g-0 p-0"
+ )
+ ]
+
def callbacks(self, app):
"""Callbacks for the whole application.
"""
@app.callback(
- Output("row-table", "children"),
- Output("input-url", "value"),
+ Output("plotting-area", "children"),
Output("period-last", "active"),
Output("period-short", "active"),
Output("period-long", "active"),
+ Input("url", "href"),
Input("period-last", "n_clicks"),
Input("period-short", "n_clicks"),
- Input("period-long", "n_clicks"),
- Input("url", "href")
+ Input("period-long", "n_clicks")
)
- def _update_application(btn_last: int, btn_short: int, btn_long: int,
- href: str) -> tuple:
+ def _update_application(href: str, *_) -> tuple:
"""Update the application when the event is detected.
:returns: New values for web page elements.
:rtype: tuple
"""
- _, _, _ = btn_last, btn_short, btn_long
-
periods = {
"period-last": C.NEWS_LAST,
"period-short": C.NEWS_SHORT,
if trigger_id == "url" and url_params:
trigger_id = url_params.get("period", list())[0]
- period = periods.get(trigger_id, self._default_period)
- active = actives.get(trigger_id, self._default_active)
-
ret_val = [
- table_summary(self._data, self._jobs, period),
- gen_new_url(parsed_url, {"period": trigger_id})
+ self._get_plotting_area(
+ periods.get(trigger_id, self._default_period),
+ gen_new_url(parsed_url, {"period": trigger_id})
+ )
]
- ret_val.extend(active)
+ ret_val.extend(actives.get(trigger_id, self._default_active))
return ret_val
+
+ @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("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