C-Dash: Fix anomaly detection for the news
[csit.git] / csit.infra.dash / app / cdash / news / layout.py
index dfe6eba..d8ad92a 100644 (file)
@@ -1,4 +1,4 @@
-# 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:
@@ -14,7 +14,6 @@
 """Plotly Dash HTML layout override.
 """
 
-import logging
 import pandas as pd
 import dash_bootstrap_components as dbc
 
@@ -22,14 +21,12 @@ from flask import Flask
 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
 
 
@@ -37,8 +34,13 @@ class Layout:
     """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,
@@ -47,34 +49,22 @@ class Layout:
         - 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(),
@@ -115,7 +105,7 @@ class Layout:
         }
         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)
@@ -143,15 +133,17 @@ class Layout:
 
             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(
@@ -196,6 +188,7 @@ class Layout:
                     )
                     if tst_data.empty:
                         continue
+                    x_axis = tst_data["start_time"].tolist()
                     try:
                         anomalies, _, _ = classify_anomalies({
                             k: v for k, v in zip(
@@ -226,7 +219,6 @@ class Layout:
 
         # Read from files:
         self._html_layout = str()
-        self._tooltips = dict()
 
         try:
             with open(self._html_layout_file, "r") as file_read:
@@ -236,23 +228,8 @@ class Layout:
                 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'):
@@ -286,7 +263,7 @@ class Layout:
                         id="row-navbar",
                         class_name="g-0",
                         children=[
-                            self._add_navbar(),
+                            self._add_navbar()
                         ]
                     ),
                     dbc.Row(
@@ -294,7 +271,7 @@ class Layout:
                         class_name="g-0",
                         children=[
                             self._add_ctrl_col(),
-                            self._add_plotting_col(),
+                            self._add_plotting_col()
                         ]
                     )
                 ]
@@ -305,10 +282,10 @@ class Layout:
                 children=[
                     dbc.Alert(
                         [
-                            "An Error Occured",
+                            "An Error Occured"
                         ],
-                        color="danger",
-                    ),
+                        color="danger"
+                    )
                 ]
             )
 
@@ -335,7 +312,7 @@ class Layout:
             brand_href="/",
             brand_external_link=True,
             class_name="p-2",
-            fluid=True,
+            fluid=True
         )
 
     def _add_ctrl_col(self) -> dbc.Col:
@@ -357,62 +334,31 @@ class Layout:
         :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=[
@@ -447,6 +393,59 @@ class Layout:
             )
         ]
 
+    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.
 
@@ -455,26 +454,22 @@ class Layout:
         """
 
         @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,
@@ -497,12 +492,23 @@ class Layout:
             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