527710fa4bb4afd53aedb3908e8cf0a6fe7952be
[csit.git] / resources / tools / dash / app / pal / news / 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 import logging
18 import pandas as pd
19 import dash_bootstrap_components as dbc
20
21 from flask import Flask
22 from dash import dcc
23 from dash import html
24 from dash import callback_context
25 from dash import Input, Output, State
26 from yaml import load, FullLoader, YAMLError
27 from copy import deepcopy
28
29 from ..data.data import Data
30 from ..utils.constants import Constants as C
31 from ..utils.utils import classify_anomalies, show_tooltip, gen_new_url
32 from ..utils.url_processing import url_decode
33 from ..data.data import Data
34 from .tables import table_news
35
36
37 class Layout:
38     """The layout of the dash app and the callbacks.
39     """
40
41     def __init__(self, app: Flask, html_layout_file: str, data_spec_file: str,
42         tooltip_file: str) -> None:
43         """Initialization:
44         - save the input parameters,
45         - read and pre-process the data,
46         - prepare data fro the control panel,
47         - read HTML layout file,
48         - read tooltips from the tooltip file.
49
50         :param app: Flask application running the dash application.
51         :param html_layout_file: Path and name of the file specifying the HTML
52             layout of the dash application.
53         :param data_spec_file: Path and name of the file specifying the data to
54             be read from parquets for this application.
55         :param tooltip_file: Path and name of the yaml file specifying the
56             tooltips.
57         :type app: Flask
58         :type html_layout_file: str
59         :type data_spec_file: str
60         :type tooltip_file: str
61         """
62
63         # Inputs
64         self._app = app
65         self._html_layout_file = html_layout_file
66         self._data_spec_file = data_spec_file
67         self._tooltip_file = tooltip_file
68
69         # Read the data:
70         data_stats, data_mrr, data_ndrpdr = Data(
71             data_spec_file=self._data_spec_file,
72             debug=True
73         ).read_stats(days=C.NEWS_TIME_PERIOD)
74
75         df_tst_info = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
76
77         # Prepare information for the control panel:
78         jobs = sorted(list(df_tst_info["job"].unique()))
79         job_info = {
80             "job": list(),
81             "dut": list(),
82             "ttype": list(),
83             "cadence": list(),
84             "tbed": list()
85         }
86         for job in jobs:
87             lst_job = job.split("-")
88             job_info["job"].append(job)
89             job_info["dut"].append(lst_job[1])
90             job_info["ttype"].append(lst_job[3])
91             job_info["cadence"].append(lst_job[4])
92             job_info["tbed"].append("-".join(lst_job[-2:]))
93         self.df_job_info = pd.DataFrame.from_dict(job_info)
94
95         self._default = self._set_job_params(C.NEWS_DEFAULT_JOB)
96
97         # Pre-process the data:
98
99         def _create_test_name(test: str) -> str:
100             lst_tst = test.split(".")
101             suite = lst_tst[-2].replace("2n1l-", "").replace("1n1l-", "").\
102                 replace("2n-", "")
103             return f"{suite.split('-')[0]}-{lst_tst[-1]}"
104
105         def _get_rindex(array: list, itm: any) -> int:
106             return len(array) - 1 - array[::-1].index(itm)
107
108         tst_info = {
109             "job": list(),
110             "build": list(),
111             "start": list(),
112             "dut_type": list(),
113             "dut_version": list(),
114             "hosts": list(),
115             "failed": list(),
116             "regressions": list(),
117             "progressions": list()
118         }
119         for job in jobs:
120             # Create lists of failed tests:
121             df_job = df_tst_info.loc[(df_tst_info["job"] == job)]
122             last_build = max(df_job["build"].unique())
123             df_build = df_job.loc[(df_job["build"] == last_build)]
124             tst_info["job"].append(job)
125             tst_info["build"].append(last_build)
126             tst_info["start"].append(data_stats.loc[
127                 (data_stats["job"] == job) &
128                 (data_stats["build"] == last_build)
129             ]["start_time"].iloc[-1].strftime('%Y-%m-%d %H:%M'))
130             tst_info["dut_type"].append(df_build["dut_type"].iloc[-1])
131             tst_info["dut_version"].append(df_build["dut_version"].iloc[-1])
132             tst_info["hosts"].append(df_build["hosts"].iloc[-1])
133             failed_tests = df_build.loc[(df_build["passed"] == False)]\
134                 ["test_id"].to_list()
135             l_failed = list()
136             try:
137                 for tst in failed_tests:
138                     l_failed.append(_create_test_name(tst))
139             except KeyError:
140                 l_failed = list()
141             tst_info["failed"].append(sorted(l_failed))
142
143             # Create lists of regressions and progressions:
144             l_reg = list()
145             l_prog = list()
146
147             tests = df_job["test_id"].unique()
148             for test in tests:
149                 tst_data = df_job.loc[df_job["test_id"] == test].sort_values(
150                     by="start_time", ignore_index=True)
151                 x_axis = tst_data["start_time"].tolist()
152                 if "-ndrpdr" in test:
153                     tst_data = tst_data.dropna(
154                         subset=["result_pdr_lower_rate_value", ]
155                     )
156                     if tst_data.empty:
157                         continue
158                     try:
159                         anomalies, _, _ = classify_anomalies({
160                             k: v for k, v in zip(
161                                 x_axis,
162                                 tst_data["result_ndr_lower_rate_value"].tolist()
163                             )
164                         })
165                     except ValueError:
166                         continue
167                     if "progression" in anomalies:
168                         l_prog.append((
169                             _create_test_name(test).replace("-ndrpdr", "-ndr"),
170                             x_axis[_get_rindex(anomalies, "progression")]
171                         ))
172                     if "regression" in anomalies:
173                         l_reg.append((
174                             _create_test_name(test).replace("-ndrpdr", "-ndr"),
175                             x_axis[_get_rindex(anomalies, "regression")]
176                         ))
177                     try:
178                         anomalies, _, _ = classify_anomalies({
179                             k: v for k, v in zip(
180                                 x_axis,
181                                 tst_data["result_pdr_lower_rate_value"].tolist()
182                             )
183                         })
184                     except ValueError:
185                         continue
186                     if "progression" in anomalies:
187                         l_prog.append((
188                             _create_test_name(test).replace("-ndrpdr", "-pdr"),
189                             x_axis[_get_rindex(anomalies, "progression")]
190                         ))
191                     if "regression" in anomalies:
192                         l_reg.append((
193                             _create_test_name(test).replace("-ndrpdr", "-pdr"),
194                             x_axis[_get_rindex(anomalies, "regression")]
195                         ))
196                 else:  # mrr
197                     tst_data = tst_data.dropna(
198                         subset=["result_receive_rate_rate_avg", ]
199                     )
200                     if tst_data.empty:
201                         continue
202                     try:
203                         anomalies, _, _ = classify_anomalies({
204                             k: v for k, v in zip(
205                                 x_axis,
206                                 tst_data["result_receive_rate_rate_avg"].\
207                                     tolist()
208                             )
209                         })
210                     except ValueError:
211                         continue
212                     if "progression" in anomalies:
213                         l_prog.append((
214                             _create_test_name(test),
215                             x_axis[_get_rindex(anomalies, "progression")]
216                         ))
217                     if "regression" in anomalies:
218                         l_reg.append((
219                             _create_test_name(test),
220                             x_axis[_get_rindex(anomalies, "regression")]
221                         ))
222
223             tst_info["regressions"].append(
224                 sorted(l_reg, key=lambda k: k[1], reverse=True))
225             tst_info["progressions"].append(
226                 sorted(l_prog, key=lambda k: k[1], reverse=True))
227
228         self._data = pd.DataFrame.from_dict(tst_info)
229
230         # Read from files:
231         self._html_layout = str()
232         self._tooltips = dict()
233
234         try:
235             with open(self._html_layout_file, "r") as file_read:
236                 self._html_layout = file_read.read()
237         except IOError as err:
238             raise RuntimeError(
239                 f"Not possible to open the file {self._html_layout_file}\n{err}"
240             )
241
242         try:
243             with open(self._tooltip_file, "r") as file_read:
244                 self._tooltips = load(file_read, Loader=FullLoader)
245         except IOError as err:
246             logging.warning(
247                 f"Not possible to open the file {self._tooltip_file}\n{err}"
248             )
249         except YAMLError as err:
250             logging.warning(
251                 f"An error occurred while parsing the specification file "
252                 f"{self._tooltip_file}\n{err}"
253             )
254
255         self._default_tab_failed = table_news(self.data, self._default["job"])
256
257         # Callbacks:
258         if self._app is not None and hasattr(self, 'callbacks'):
259             self.callbacks(self._app)
260
261     @property
262     def html_layout(self) -> dict:
263         return self._html_layout
264
265     @property
266     def data(self) -> pd.DataFrame:
267         return self._data
268
269     @property
270     def default(self) -> dict:
271         return self._default
272
273     def _get_duts(self) -> list:
274         """Get the list of DUTs from the pre-processed information about jobs.
275
276         :returns: Alphabeticaly sorted list of DUTs.
277         :rtype: list
278         """
279         return sorted(list(self.df_job_info["dut"].unique()))
280
281     def _get_ttypes(self, dut: str) -> list:
282         """Get the list of test types from the pre-processed information about
283         jobs.
284
285         :param dut: The DUT for which the list of test types will be populated.
286         :type dut: str
287         :returns: Alphabeticaly sorted list of test types.
288         :rtype: list
289         """
290         return sorted(list(self.df_job_info.loc[(
291             self.df_job_info["dut"] == dut
292         )]["ttype"].unique()))
293
294     def _get_cadences(self, dut: str, ttype: str) -> list:
295         """Get the list of cadences from the pre-processed information about
296         jobs.
297
298         :param dut: The DUT for which the list of cadences will be populated.
299         :param ttype: The test type for which the list of cadences will be
300             populated.
301         :type dut: str
302         :type ttype: str
303         :returns: Alphabeticaly sorted list of cadences.
304         :rtype: list
305         """
306         return sorted(list(self.df_job_info.loc[(
307             (self.df_job_info["dut"] == dut) &
308             (self.df_job_info["ttype"] == ttype)
309         )]["cadence"].unique()))
310
311     def _get_test_beds(self, dut: str, ttype: str, cadence: str) -> list:
312         """Get the list of test beds from the pre-processed information about
313         jobs.
314
315         :param dut: The DUT for which the list of test beds will be populated.
316         :param ttype: The test type for which the list of test beds will be
317             populated.
318         :param cadence: The cadence for which the list of test beds will be
319             populated.
320         :type dut: str
321         :type ttype: str
322         :type cadence: str
323         :returns: Alphabeticaly sorted list of test beds.
324         :rtype: list
325         """
326         return sorted(list(self.df_job_info.loc[(
327             (self.df_job_info["dut"] == dut) &
328             (self.df_job_info["ttype"] == ttype) &
329             (self.df_job_info["cadence"] == cadence)
330         )]["tbed"].unique()))
331
332     def _get_job(self, dut, ttype, cadence, testbed):
333         """Get the name of a job defined by dut, ttype, cadence, test bed.
334         Input information comes from the control panel.
335
336         :param dut: The DUT for which the job name will be created.
337         :param ttype: The test type for which the job name will be created.
338         :param cadence: The cadence for which the job name will be created.
339         :param testbed: The test bed for which the job name will be created.
340         :type dut: str
341         :type ttype: str
342         :type cadence: str
343         :type testbed: str
344         :returns: Job name.
345         :rtype: str
346         """
347         return self.df_job_info.loc[(
348             (self.df_job_info["dut"] == dut) &
349             (self.df_job_info["ttype"] == ttype) &
350             (self.df_job_info["cadence"] == cadence) &
351             (self.df_job_info["tbed"] == testbed)
352         )]["job"].item()
353
354     @staticmethod
355     def _generate_options(opts: list) -> list:
356         """Return list of options for radio items in control panel. The items in
357         the list are dictionaries with keys "label" and "value".
358
359         :params opts: List of options (str) to be used for the generated list.
360         :type opts: list
361         :returns: List of options (dict).
362         :rtype: list
363         """
364         return [{"label": i, "value": i} for i in opts]
365
366     def _set_job_params(self, job: str) -> dict:
367         """Create a dictionary with all options and values for (and from) the
368         given job.
369
370         :params job: The name of job for and from which the dictionary will be
371             created.
372         :type job: str
373         :returns: Dictionary with all options and values for (and from) the
374             given job.
375         :rtype: dict
376         """
377
378         lst_job = job.split("-")
379         return {
380             "job": job,
381             "dut": lst_job[1],
382             "ttype": lst_job[3],
383             "cadence": lst_job[4],
384             "tbed": "-".join(lst_job[-2:]),
385             "duts": self._generate_options(self._get_duts()),
386             "ttypes": self._generate_options(self._get_ttypes(lst_job[1])),
387             "cadences": self._generate_options(self._get_cadences(
388                 lst_job[1], lst_job[3])),
389             "tbeds": self._generate_options(self._get_test_beds(
390                 lst_job[1], lst_job[3], lst_job[4]))
391         }
392
393     def add_content(self):
394         """Top level method which generated the web page.
395
396         It generates:
397         - Store for user input data,
398         - Navigation bar,
399         - Main area with control panel and ploting area.
400
401         If no HTML layout is provided, an error message is displayed instead.
402
403         :returns: The HTML div with teh whole page.
404         :rtype: html.Div
405         """
406
407         if self.html_layout:
408             return html.Div(
409                 id="div-main",
410                 children=[
411                     dcc.Store(id="control-panel"),
412                     dcc.Location(id="url", refresh=False),
413                     dbc.Row(
414                         id="row-navbar",
415                         class_name="g-0",
416                         children=[
417                             self._add_navbar(),
418                         ]
419                     ),
420                     dbc.Row(
421                         id="row-main",
422                         class_name="g-0",
423                         children=[
424                             self._add_ctrl_col(),
425                             self._add_plotting_col(),
426                         ]
427                     )
428                 ]
429             )
430         else:
431             return html.Div(
432                 id="div-main-error",
433                 children=[
434                     dbc.Alert(
435                         [
436                             "An Error Occured",
437                         ],
438                         color="danger",
439                     ),
440                 ]
441             )
442
443     def _add_navbar(self):
444         """Add nav element with navigation panel. It is placed on the top.
445
446         :returns: Navigation bar.
447         :rtype: dbc.NavbarSimple
448         """
449
450         return dbc.NavbarSimple(
451             id="navbarsimple-main",
452             children=[
453                 dbc.NavItem(
454                     dbc.NavLink(
455                         "Continuous Performance News",
456                         disabled=True,
457                         external_link=True,
458                         href="#"
459                     )
460                 )
461             ],
462             brand="Dashboard",
463             brand_href="/",
464             brand_external_link=True,
465             class_name="p-2",
466             fluid=True,
467         )
468
469     def _add_ctrl_col(self) -> dbc.Col:
470         """Add column with control panel. It is placed on the left side.
471
472         :returns: Column with the control panel.
473         :rtype: dbc.col
474         """
475
476         return dbc.Col(
477             id="col-controls",
478             children=[
479                 self._add_ctrl_panel(),
480             ],
481         )
482
483     def _add_plotting_col(self) -> dbc.Col:
484         """Add column with tables. It is placed on the right side.
485
486         :returns: Column with tables.
487         :rtype: dbc.col
488         """
489
490         return dbc.Col(
491             id="col-plotting-area",
492             children=[
493                 dbc.Row(  # Failed tests
494                     id="row-table-failed",
495                     class_name="g-0 p-2",
496                     children=self._default_tab_failed
497                 ),
498                 dbc.Row(
499                     class_name="g-0 p-2",
500                     align="center",
501                     justify="start",
502                     children=[
503                         dbc.InputGroup(
504                             class_name="me-1",
505                             children=[
506                                 dbc.InputGroupText(
507                                     style=C.URL_STYLE,
508                                     children=show_tooltip(
509                                         self._tooltips,
510                                         "help-url", "URL",
511                                         "input-url"
512                                     )
513                                 ),
514                                 dbc.Input(
515                                     id="input-url",
516                                     readonly=True,
517                                     type="url",
518                                     style=C.URL_STYLE,
519                                     value=""
520                                 )
521                             ]
522                         )
523                     ]
524                 )
525             ],
526             width=9,
527         )
528
529     def _add_ctrl_panel(self) -> dbc.Row:
530         """Add control panel.
531
532         :returns: Control panel.
533         :rtype: dbc.Row
534         """
535         return dbc.Row(
536             id="row-ctrl-panel",
537             class_name="g-0",
538             children=[
539                 dbc.Row(
540                     class_name="g-0 p-2",
541                     children=[
542                         dbc.Row(
543                             class_name="gy-1",
544                             children=[
545                                 dbc.Label(
546                                     class_name="p-0",
547                                     children=show_tooltip(self._tooltips,
548                                         "help-dut", "Device under Test")
549                                 ),
550                                 dbc.Row(
551                                     dbc.RadioItems(
552                                         id="ri-duts",
553                                         inline=True,
554                                         value=self.default["dut"],
555                                         options=self.default["duts"]
556                                     )
557                                 )
558                             ]
559                         ),
560                         dbc.Row(
561                             class_name="gy-1",
562                             children=[
563                                 dbc.Label(
564                                     class_name="p-0",
565                                     children=show_tooltip(self._tooltips,
566                                         "help-ttype", "Test Type"),
567                                 ),
568                                 dbc.RadioItems(
569                                     id="ri-ttypes",
570                                     inline=True,
571                                     value=self.default["ttype"],
572                                     options=self.default["ttypes"]
573                                 )
574                             ]
575                         ),
576                         dbc.Row(
577                             class_name="gy-1",
578                             children=[
579                                 dbc.Label(
580                                     class_name="p-0",
581                                     children=show_tooltip(self._tooltips,
582                                         "help-cadence", "Cadence"),
583                                 ),
584                                 dbc.RadioItems(
585                                     id="ri-cadences",
586                                     inline=True,
587                                     value=self.default["cadence"],
588                                     options=self.default["cadences"]
589                                 )
590                             ]
591                         ),
592                         dbc.Row(
593                             class_name="gy-1",
594                             children=[
595                                 dbc.Label(
596                                     class_name="p-0",
597                                     children=show_tooltip(self._tooltips,
598                                         "help-tbed", "Test Bed"),
599                                 ),
600                                 dbc.Select(
601                                     id="dd-tbeds",
602                                     placeholder="Select a test bed...",
603                                     value=self.default["tbed"],
604                                     options=self.default["tbeds"]
605                                 )
606                             ]
607                         ),
608                         dbc.Row(
609                             class_name="gy-1",
610                             children=[
611                                 dbc.Alert(
612                                     id="al-job",
613                                     color="info",
614                                     children=self.default["job"]
615                                 )
616                             ]
617                         )
618                     ]
619                 ),
620             ]
621         )
622
623     class ControlPanel:
624         """
625         """
626
627         def __init__(self, panel: dict, default: dict) -> None:
628             """
629             """
630
631             self._defaults = {
632                 "ri-ttypes-options": default["ttypes"],
633                 "ri-cadences-options": default["cadences"],
634                 "dd-tbeds-options": default["tbeds"],
635                 "ri-duts-value": default["dut"],
636                 "ri-ttypes-value": default["ttype"],
637                 "ri-cadences-value": default["cadence"],
638                 "dd-tbeds-value": default["tbed"],
639                 "al-job-children": default["job"]
640             }
641             self._panel = deepcopy(self._defaults)
642             if panel:
643                 for key in self._defaults:
644                     self._panel[key] = panel[key]
645
646         def set(self, kwargs: dict) -> None:
647             for key, val in kwargs.items():
648                 if key in self._panel:
649                     self._panel[key] = val
650                 else:
651                     raise KeyError(f"The key {key} is not defined.")
652
653         @property
654         def defaults(self) -> dict:
655             return self._defaults
656
657         @property
658         def panel(self) -> dict:
659             return self._panel
660
661         def get(self, key: str) -> any:
662             return self._panel[key]
663
664         def values(self) -> list:
665             return list(self._panel.values())
666
667     def callbacks(self, app):
668
669         @app.callback(
670             Output("control-panel", "data"),  # Store
671             Output("row-table-failed", "children"),
672             Output("input-url", "value"),
673             Output("ri-ttypes", "options"),
674             Output("ri-cadences", "options"),
675             Output("dd-tbeds", "options"),
676             Output("ri-duts", "value"),
677             Output("ri-ttypes", "value"),
678             Output("ri-cadences", "value"),
679             Output("dd-tbeds", "value"),
680             Output("al-job", "children"),
681             State("control-panel", "data"),  # Store
682             Input("ri-duts", "value"),
683             Input("ri-ttypes", "value"),
684             Input("ri-cadences", "value"),
685             Input("dd-tbeds", "value"),
686             Input("url", "href")
687         )
688         def _update_ctrl_panel(cp_data: dict, dut:str, ttype: str, cadence:str,
689                 tbed: str, href: str) -> tuple:
690             """
691             """
692
693             ctrl_panel = self.ControlPanel(cp_data, self.default)
694
695             # Parse the url:
696             parsed_url = url_decode(href)
697             if parsed_url:
698                 url_params = parsed_url["params"]
699             else:
700                 url_params = None
701
702             trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
703             if trigger_id == "ri-duts":
704                 ttype_opts = self._generate_options(self._get_ttypes(dut))
705                 ttype_val = ttype_opts[0]["value"]
706                 cad_opts = self._generate_options(
707                     self._get_cadences(dut, ttype_val))
708                 cad_val = cad_opts[0]["value"]
709                 tbed_opts = self._generate_options(
710                     self._get_test_beds(dut, ttype_val, cad_val))
711                 tbed_val = tbed_opts[0]["value"]
712                 ctrl_panel.set({
713                     "ri-duts-value": dut,
714                     "ri-ttypes-options": ttype_opts,
715                     "ri-ttypes-value": ttype_val,
716                     "ri-cadences-options": cad_opts,
717                     "ri-cadences-value": cad_val,
718                     "dd-tbeds-options": tbed_opts,
719                     "dd-tbeds-value": tbed_val
720                 })
721             elif trigger_id == "ri-ttypes":
722                 cad_opts = self._generate_options(
723                     self._get_cadences(ctrl_panel.get("ri-duts-value"), ttype))
724                 cad_val = cad_opts[0]["value"]
725                 tbed_opts = self._generate_options(
726                     self._get_test_beds(ctrl_panel.get("ri-duts-value"),
727                     ttype, cad_val))
728                 tbed_val = tbed_opts[0]["value"]
729                 ctrl_panel.set({
730                     "ri-ttypes-value": ttype,
731                     "ri-cadences-options": cad_opts,
732                     "ri-cadences-value": cad_val,
733                     "dd-tbeds-options": tbed_opts,
734                     "dd-tbeds-value": tbed_val
735                 })
736             elif trigger_id == "ri-cadences":
737                 tbed_opts = self._generate_options(
738                     self._get_test_beds(ctrl_panel.get("ri-duts-value"),
739                     ctrl_panel.get("ri-ttypes-value"), cadence))
740                 tbed_val = tbed_opts[0]["value"]
741                 ctrl_panel.set({
742                     "ri-cadences-value": cadence,
743                     "dd-tbeds-options": tbed_opts,
744                     "dd-tbeds-value": tbed_val
745                 })
746             elif trigger_id == "dd-tbeds":
747                 ctrl_panel.set({
748                     "dd-tbeds-value": tbed
749                 })
750             elif trigger_id == "url":
751                 # TODO: Add verification
752                 if url_params:
753                     new_job = url_params.get("job", list())[0]
754                     if new_job:
755                         job_params = self._set_job_params(new_job)
756                         ctrl_panel = self.ControlPanel(None, job_params)
757                 else:
758                     ctrl_panel = self.ControlPanel(cp_data, self.default)
759                     job = self._get_job(
760                         ctrl_panel.get("ri-duts-value"),
761                         ctrl_panel.get("ri-ttypes-value"),
762                         ctrl_panel.get("ri-cadences-value"),
763                         ctrl_panel.get("dd-tbeds-value")
764                     )
765
766             job = self._get_job(
767                 ctrl_panel.get("ri-duts-value"),
768                 ctrl_panel.get("ri-ttypes-value"),
769                 ctrl_panel.get("ri-cadences-value"),
770                 ctrl_panel.get("dd-tbeds-value")
771             )
772             ctrl_panel.set({"al-job-children": job})
773             tab_failed = table_news(self.data, job)
774
775             ret_val = [
776                 ctrl_panel.panel,
777                 tab_failed,
778                 gen_new_url(parsed_url, {"job": job})
779             ]
780             ret_val.extend(ctrl_panel.values())
781             return ret_val