UTI: PoC - Make detailed hover information copyable
[csit.git] / resources / tools / dash / app / pal / trending / layout.py
1 # Copyright (c) 2022 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:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
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.
13
14 """Plotly Dash HTML layout override.
15 """
16
17
18 import json
19 import pandas as pd
20
21 from dash import dcc
22 from dash import html
23 from dash import callback_context, no_update
24 from dash import Input, Output, State, callback
25 from dash.exceptions import PreventUpdate
26 from yaml import load, FullLoader, YAMLError
27 from datetime import datetime, timedelta
28
29 from ..data.data import Data
30 from .graphs import trending_tput
31
32
33 class Layout:
34     """
35     """
36
37     def __init__(self, app, html_layout_file, spec_file, graph_layout_file,
38         data_spec_file):
39         """
40         """
41
42         # Inputs
43         self._app = app
44         self._html_layout_file = html_layout_file
45         self._spec_file = spec_file
46         self._graph_layout_file = graph_layout_file
47         self._data_spec_file = data_spec_file
48
49         # Read the data:
50         data_mrr = Data(
51             data_spec_file=self._data_spec_file,
52             debug=True
53         ).read_trending_mrr()
54
55         data_ndrpdr = Data(
56             data_spec_file=self._data_spec_file,
57             debug=True
58         ).read_trending_ndrpdr()
59
60         self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
61
62         # Read from files:
63         self._html_layout = ""
64         self._spec_tbs = None
65         self._graph_layout = None
66
67         try:
68             with open(self._html_layout_file, "r") as file_read:
69                 self._html_layout = file_read.read()
70         except IOError as err:
71             raise RuntimeError(
72                 f"Not possible to open the file {self._html_layout_file}\n{err}"
73             )
74
75         try:
76             with open(self._spec_file, "r") as file_read:
77                 self._spec_tbs = load(file_read, Loader=FullLoader)
78         except IOError as err:
79             raise RuntimeError(
80                 f"Not possible to open the file {self._spec_file,}\n{err}"
81             )
82         except YAMLError as err:
83             raise RuntimeError(
84                 f"An error occurred while parsing the specification file "
85                 f"{self._spec_file,}\n"
86                 f"{err}"
87             )
88
89         try:
90             with open(self._graph_layout_file, "r") as file_read:
91                 self._graph_layout = load(file_read, Loader=FullLoader)
92         except IOError as err:
93             raise RuntimeError(
94                 f"Not possible to open the file {self._graph_layout_file}\n"
95                 f"{err}"
96             )
97         except YAMLError as err:
98             raise RuntimeError(
99                 f"An error occurred while parsing the specification file "
100                 f"{self._graph_layout_file}\n"
101                 f"{err}"
102             )
103
104         # Callbacks:
105         if self._app is not None and hasattr(self, 'callbacks'):
106             self.callbacks(self._app)
107
108     @property
109     def html_layout(self):
110         return self._html_layout
111
112     @property
113     def spec_tbs(self):
114         return self._spec_tbs
115
116     @property
117     def data(self):
118         return self._data
119
120     @property
121     def layout(self):
122         return self._graph_layout
123
124     def add_content(self):
125         """
126         """
127         if self.html_layout and self.spec_tbs:
128             return html.Div(
129                 id="div-main",
130                 children=[
131                     dcc.Store(id="selected-tests"),
132                     self._add_ctrl_div(),
133                     self._add_plotting_div()
134                 ]
135             )
136         else:
137             return html.Div(
138             id="div-main-error",
139             children="An Error Occured."
140         )
141
142     def _add_ctrl_div(self):
143         """Add div with controls. It is placed on the left side.
144         """
145         return html.Div(
146             id="div-controls",
147             children=[
148                 html.Div(
149                     id="div-controls-tabs",
150                     children=[
151                         self._add_ctrl_select(),
152                         self._add_ctrl_shown()
153                     ]
154                 )
155             ],
156             style={
157                 "display": "inline-block",
158                 "width": "18%",
159                 "padding": "5px"
160             }
161         )
162
163     def _add_plotting_div(self):
164         """Add div with plots and tables. It is placed on the right side.
165         """
166         return html.Div(
167             id="div-plotting-area",
168             children=[
169                 dcc.Loading(
170                     id="loading-graph",
171                     children=[
172                         dcc.Graph(
173                             id="graph"
174                         )
175                     ],
176                     type="circle"
177                 ),
178                 html.Div(
179                     children=[
180                         dcc.Markdown("""
181                         **Metadata**
182
183                         Click on data points in the graph.
184                         """),
185                         html.Pre(
186                             id="hover-metadata"
187                         )
188                     ]
189                 )
190             ],
191             style={
192                 "vertical-align": "top",
193                 "display": "none",
194                 "width": "80%",
195                 "padding": "5px"
196             }
197         )
198
199     def _add_ctrl_shown(self):
200         """
201         """
202         return html.Div(
203             id="div-ctrl-shown",
204             children=[
205                 html.H5("Selected tests"),
206                 html.Div(
207                     id="container-selected-tests",
208                     children=[
209                         dcc.Checklist(
210                             id="cl-selected",
211                             options=[],
212                             labelStyle={"display": "block"}
213                         ),
214                         html.Button(
215                             id="btn-sel-remove",
216                             children="Remove Selected",
217                             disabled=False
218                         ),
219                         html.Button(
220                             id="btn-sel-display",
221                             children="Display",
222                             disabled=False
223                         )
224                     ]
225                 ),
226             ]
227         )
228
229     def _add_ctrl_select(self):
230         """
231         """
232         return html.Div(
233             id="div-ctrl-select",
234             children=[
235                 html.H5("Physical Test Bed Topology, NIC and Driver"),
236                 dcc.Dropdown(
237                     id="dd-ctrl-phy",
238                     placeholder="Select a Physical Test Bed Topology...",
239                     multi=False,
240                     clearable=False,
241                     options=[
242                         {"label": k, "value": k} for k in self.spec_tbs.keys()
243                     ],
244                 ),
245                 html.H5("Area"),
246                 dcc.Dropdown(
247                     id="dd-ctrl-area",
248                     placeholder="Select an Area...",
249                     disabled=True,
250                     multi=False,
251                     clearable=False,
252                 ),
253                 html.H5("Test"),
254                 dcc.Dropdown(
255                     id="dd-ctrl-test",
256                     placeholder="Select a Test...",
257                     disabled=True,
258                     multi=False,
259                     clearable=False,
260                 ),
261                 html.Div(
262                     id="div-ctrl-core",
263                     children=[
264                         html.H5("Number of Cores"),
265                         dcc.Checklist(
266                             id="cl-ctrl-core-all",
267                             options=[{"label": "All", "value": "all"}, ],
268                             labelStyle={"display": "inline-block"}
269                         ),
270                         dcc.Checklist(
271                             id="cl-ctrl-core",
272                             labelStyle={"display": "inline-block"}
273                         )
274                     ],
275                     style={"display": "none"}
276                 ),
277                 html.Div(
278                     id="div-ctrl-framesize",
279                     children=[
280                         html.H5("Frame Size"),
281                         dcc.Checklist(
282                             id="cl-ctrl-framesize-all",
283                             options=[{"label": "All", "value": "all"}, ],
284                             labelStyle={"display": "inline-block"}
285                         ),
286                         dcc.Checklist(
287                             id="cl-ctrl-framesize",
288                             labelStyle={"display": "inline-block"}
289                         )
290                     ],
291                     style={"display": "none"}
292                 ),
293                 html.Div(
294                     id="div-ctrl-testtype",
295                     children=[
296                         html.H5("Test Type"),
297                         dcc.Checklist(
298                             id="cl-ctrl-testtype-all",
299                             options=[{"label": "All", "value": "all"}, ],
300                             labelStyle={"display": "inline-block"}
301                         ),
302                         dcc.Checklist(
303                             id="cl-ctrl-testtype",
304                             labelStyle={"display": "inline-block"}
305                         )
306                     ],
307                     style={"display": "none"}
308                 ),
309                 html.Button(
310                     id="btn-ctrl-add",
311                     children="Add",
312                     disabled=True
313                 ),
314                 html.Br(),
315                 dcc.DatePickerRange(
316                     id="dpr-period",
317                     min_date_allowed=datetime.utcnow() - timedelta(days=180),
318                     max_date_allowed=datetime.utcnow(),
319                     initial_visible_month=datetime.utcnow(),
320                     start_date=datetime.utcnow() - timedelta(days=180),
321                     end_date=datetime.utcnow(),
322                     display_format="D MMMM YY"
323                 )
324             ]
325         )
326
327     def callbacks(self, app):
328
329         @app.callback(
330             Output("dd-ctrl-area", "options"),
331             Output("dd-ctrl-area", "disabled"),
332             Input("dd-ctrl-phy", "value"),
333         )
334         def _update_dd_area(phy):
335             """
336             """
337
338             if phy is None:
339                 raise PreventUpdate
340
341             try:
342                 options = [
343                     {"label": self.spec_tbs[phy][v]["label"], "value": v}
344                         for v in [v for v in self.spec_tbs[phy].keys()]
345                 ]
346                 disable = False
347             except KeyError:
348                 options = list()
349                 disable = True
350
351             return options, disable
352
353         @app.callback(
354             Output("dd-ctrl-test", "options"),
355             Output("dd-ctrl-test", "disabled"),
356             State("dd-ctrl-phy", "value"),
357             Input("dd-ctrl-area", "value"),
358         )
359         def _update_dd_test(phy, area):
360             """
361             """
362
363             if not area:
364                 raise PreventUpdate
365
366             try:
367                 options = [
368                     {"label": v, "value": v}
369                         for v in self.spec_tbs[phy][area]["test"]
370                 ]
371                 disable = False
372             except KeyError:
373                 options = list()
374                 disable = True
375
376             return options, disable
377
378         @app.callback(
379             Output("div-ctrl-core", "style"),
380             Output("cl-ctrl-core", "options"),
381             Output("div-ctrl-framesize", "style"),
382             Output("cl-ctrl-framesize", "options"),
383             Output("div-ctrl-testtype", "style"),
384             Output("cl-ctrl-testtype", "options"),
385             Output("btn-ctrl-add", "disabled"),
386             State("dd-ctrl-phy", "value"),
387             State("dd-ctrl-area", "value"),
388             Input("dd-ctrl-test", "value"),
389         )
390         def _update_btn_add(phy, area, test):
391             """
392             """
393
394             if test is None:
395                 raise PreventUpdate
396
397             core_style = {"display": "none"}
398             core_opts = []
399             framesize_style = {"display": "none"}
400             framesize_opts = []
401             testtype_style = {"display": "none"}
402             testtype_opts = []
403             add_disabled = True
404             if phy and area and test:
405                 core_style = {"display": "block"}
406                 core_opts = [
407                     {"label": v, "value": v}
408                         for v in self.spec_tbs[phy][area]["core"]
409                 ]
410                 framesize_style = {"display": "block"}
411                 framesize_opts = [
412                     {"label": v, "value": v}
413                         for v in self.spec_tbs[phy][area]["frame-size"]
414                 ]
415                 testtype_style = {"display": "block"}
416                 testtype_opts = [
417                     {"label": v, "value": v}
418                         for v in self.spec_tbs[phy][area]["test-type"]
419                 ]
420                 add_disabled = False
421
422             return (
423                 core_style, core_opts,
424                 framesize_style, framesize_opts,
425                 testtype_style, testtype_opts,
426                 add_disabled
427             )
428
429         def _sync_checklists(opt, sel, all, id):
430             """
431             """
432             options = {v["value"] for v in opt}
433             input_id = callback_context.triggered[0]["prop_id"].split(".")[0]
434             if input_id == id:
435                 all = ["all"] if set(sel) == options else list()
436             else:
437                 sel = list(options) if all else list()
438             return sel, all
439
440         @app.callback(
441             Output("cl-ctrl-core", "value"),
442             Output("cl-ctrl-core-all", "value"),
443             State("cl-ctrl-core", "options"),
444             Input("cl-ctrl-core", "value"),
445             Input("cl-ctrl-core-all", "value"),
446             prevent_initial_call=True
447         )
448         def _sync_cl_core(opt, sel, all):
449             return _sync_checklists(opt, sel, all, "cl-ctrl-core")
450
451         @app.callback(
452             Output("cl-ctrl-framesize", "value"),
453             Output("cl-ctrl-framesize-all", "value"),
454             State("cl-ctrl-framesize", "options"),
455             Input("cl-ctrl-framesize", "value"),
456             Input("cl-ctrl-framesize-all", "value"),
457             prevent_initial_call=True
458         )
459         def _sync_cl_framesize(opt, sel, all):
460             return _sync_checklists(opt, sel, all, "cl-ctrl-framesize")
461
462         @app.callback(
463             Output("cl-ctrl-testtype", "value"),
464             Output("cl-ctrl-testtype-all", "value"),
465             State("cl-ctrl-testtype", "options"),
466             Input("cl-ctrl-testtype", "value"),
467             Input("cl-ctrl-testtype-all", "value"),
468             prevent_initial_call=True
469         )
470         def _sync_cl_testtype(opt, sel, all):
471             return _sync_checklists(opt, sel, all, "cl-ctrl-testtype")
472
473         @app.callback(
474             Output("graph", "figure"),
475             Output("selected-tests", "data"),  # Store
476             Output("cl-selected", "options"),  # User selection
477             Output("dd-ctrl-phy", "value"),
478             Output("dd-ctrl-area", "value"),
479             Output("dd-ctrl-test", "value"),
480             Output("div-plotting-area", "style"),
481             State("selected-tests", "data"),  # Store
482             State("cl-selected", "value"),
483             State("dd-ctrl-phy", "value"),
484             State("dd-ctrl-area", "value"),
485             State("dd-ctrl-test", "value"),
486             State("cl-ctrl-core", "value"),
487             State("cl-ctrl-framesize", "value"),
488             State("cl-ctrl-testtype", "value"),
489             Input("btn-ctrl-add", "n_clicks"),
490             Input("btn-sel-display", "n_clicks"),
491             Input("btn-sel-remove", "n_clicks"),
492             Input("dpr-period", "start_date"),
493             Input("dpr-period", "end_date"),
494             prevent_initial_call=True
495         )
496         def _process_list(store_sel, list_sel, phy, area, test, cores,
497                 framesizes, testtypes, btn_add, btn_display, btn_remove,
498                 d_start, d_end):
499             """
500             """
501
502             if not (btn_add or btn_display or btn_remove or d_start or d_end):
503                 raise PreventUpdate
504
505             def _list_tests():
506                 # Display selected tests with checkboxes:
507                 if store_sel:
508                     return [
509                         {"label": v["id"], "value": v["id"]} for v in store_sel
510                     ]
511                 else:
512                     return list()
513
514             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
515
516             d_start = datetime(
517                  int(d_start[0:4]), int(d_start[5:7]), int(d_start[8:10])
518             )
519             d_end = datetime(
520                  int(d_end[0:4]), int(d_end[5:7]), int(d_end[8:10])
521             )
522
523             if trigger_id == "btn-ctrl-add":
524                 # Add selected test to the list of tests in store:
525                 if phy and area and test and cores and framesizes and testtypes:
526
527                     # TODO: Add validation
528
529                     if store_sel is None:
530                         store_sel = list()
531
532                     for core in cores:
533                         for framesize in framesizes:
534                             for ttype in testtypes:
535                                 tid = (
536                                     f"{phy.replace('af_xdp', 'af-xdp')}-"
537                                     f"{area}-"
538                                     f"{framesize.lower()}-"
539                                     f"{core.lower()}-"
540                                     f"{test}-"
541                                     f"{ttype.lower()}"
542                                 )
543                                 if tid not in [itm["id"] for itm in store_sel]:
544                                     store_sel.append({
545                                         "id": tid,
546                                         "phy": phy,
547                                         "area": area,
548                                         "test": test,
549                                         "framesize": framesize.lower(),
550                                         "core": core.lower(),
551                                         "testtype": ttype.lower()
552                                     })
553                 return (no_update, store_sel, _list_tests(), None,
554                     None, None, no_update)
555
556             elif trigger_id in ("btn-sel-display", "dpr-period"):
557                 fig, style = trending_tput(
558                     self.data, store_sel, self.layout, d_start, d_end
559                 )
560                 return (fig, no_update, no_update,
561                     no_update, no_update, no_update, style)
562
563             elif trigger_id == "btn-sel-remove":
564                 if list_sel:
565                     new_store_sel = list()
566                     for item in store_sel:
567                         if item["id"] not in list_sel:
568                             new_store_sel.append(item)
569                     store_sel = new_store_sel
570                 if store_sel:
571                     fig, style = trending_tput(
572                         self.data, store_sel, self.layout, d_start, d_end
573                     )
574                     return (fig, store_sel, _list_tests(),
575                     no_update, no_update, no_update, style)
576                 else:
577                     style={
578                         "vertical-align": "top",
579                         "display": "none",
580                         "width": "80%",
581                         "padding": "5px"
582                     }
583                     return (no_update, store_sel, _list_tests(),
584                         no_update, no_update, no_update, style)
585
586         @app.callback(
587             Output("hover-metadata", "children"),
588             Input("graph", "clickData")
589         )
590         def _show_metadata(hover_data):
591             if not hover_data:
592                 raise PreventUpdate
593             return json.dumps(hover_data, indent=2)