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