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