robotframework-dashboard 2.0.0__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {robotframework_dashboard-2.0.0/robotframework_dashboard.egg-info → robotframework_dashboard-2.1.0}/PKG-INFO +1 -1
  2. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/arguments.py +3 -0
  3. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/css/components.css +62 -0
  4. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/database.py +54 -1
  5. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/admin_page/admin_api.js +3 -0
  6. robotframework_dashboard-2.1.0/robotframework_dashboard/js/customsections.js +224 -0
  7. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/eventlisteners.js +100 -5
  8. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/filter.js +120 -3
  9. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_creation/keyword.js +15 -3
  10. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_creation/suite.js +15 -2
  11. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_creation/test.js +28 -6
  12. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/layout.js +33 -0
  13. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/localstorage.js +4 -2
  14. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/variables/globals.js +7 -0
  15. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/variables/information.js +9 -0
  16. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/variables/settings.js +3 -0
  17. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/server.py +28 -6
  18. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/templates/admin.html +11 -1
  19. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/templates/dashboard.html +74 -1
  20. robotframework_dashboard-2.1.0/robotframework_dashboard/version.py +1 -0
  21. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0/robotframework_dashboard.egg-info}/PKG-INFO +1 -1
  22. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard.egg-info/SOURCES.txt +1 -0
  23. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/setup.py +1 -1
  24. robotframework_dashboard-2.0.0/robotframework_dashboard/version.py +0 -1
  25. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/LICENSE +0 -0
  26. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/MANIFEST.in +0 -0
  27. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/README.md +0 -0
  28. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/pyproject.toml +0 -0
  29. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/__init__.py +0 -0
  30. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/abstractdb.py +0 -0
  31. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/css/base.css +0 -0
  32. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/css/colors.css +0 -0
  33. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/css/dark.css +0 -0
  34. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dashboard.py +0 -0
  35. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/bootstrap.css +0 -0
  36. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/bootstrap.js +0 -0
  37. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/chart.js +0 -0
  38. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/chartjs-adapter-date-fns.js +0 -0
  39. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/chartjs-chart-boxplot.js +0 -0
  40. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/chartjs-chart-matrix.js +0 -0
  41. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/chartjs-plugin-datalabels.js +0 -0
  42. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/datatables.css +0 -0
  43. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/datatables.js +0 -0
  44. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/gridstack.css +0 -0
  45. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/gridstack.js +0 -0
  46. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies/pako.js +0 -0
  47. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/dependencies.py +0 -0
  48. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/admin_page/admin_common.js +0 -0
  49. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/admin_page/admin_eventlisteners.js +0 -0
  50. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/admin_page/admin_information.js +0 -0
  51. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/admin_page/admin_main.js +0 -0
  52. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/admin_page/admin_theme.js +0 -0
  53. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/common.js +0 -0
  54. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/database.js +0 -0
  55. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_creation/all.js +0 -0
  56. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_creation/chart_factory.js +0 -0
  57. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_creation/compare.js +0 -0
  58. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_creation/config_helpers.js +0 -0
  59. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_creation/overview.js +0 -0
  60. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_creation/run.js +0 -0
  61. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_creation/tables.js +0 -0
  62. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/donut.js +0 -0
  63. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/duration.js +0 -0
  64. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/duration_deviation.js +0 -0
  65. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/failed.js +0 -0
  66. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/flaky.js +0 -0
  67. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/graph_config.js +0 -0
  68. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/heatmap.js +0 -0
  69. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/helpers.js +0 -0
  70. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/messages.js +0 -0
  71. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/statistics.js +0 -0
  72. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/stats.js +0 -0
  73. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/time_consuming.js +0 -0
  74. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/graph_data/tooltip_helpers.js +0 -0
  75. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/information.js +0 -0
  76. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/log.js +0 -0
  77. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/main.js +0 -0
  78. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/menu.js +0 -0
  79. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/statwidgets.js +0 -0
  80. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/theme.js +0 -0
  81. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/variables/chartconfig.js +0 -0
  82. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/variables/data.js +0 -0
  83. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/variables/graphmetadata.js +0 -0
  84. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/variables/graphs.js +0 -0
  85. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/variables/statwidgetdefs.js +0 -0
  86. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/js/variables/svg.js +0 -0
  87. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/licenses/THIRD_PARTY_LICENSES.txt +0 -0
  88. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/main.py +0 -0
  89. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/processors.py +0 -0
  90. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/queries.py +0 -0
  91. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard/robotdashboard.py +0 -0
  92. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard.egg-info/dependency_links.txt +0 -0
  93. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard.egg-info/entry_points.txt +0 -0
  94. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard.egg-info/not-zip-safe +0 -0
  95. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard.egg-info/requires.txt +0 -0
  96. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/robotframework_dashboard.egg-info/top_level.txt +0 -0
  97. {robotframework_dashboard-2.0.0 → robotframework_dashboard-2.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-dashboard
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Output processor and dashboard generator for Robot Framework output files
5
5
  Home-page: https://github.com/marketsquare/robotframework-dashboard
6
6
  Author: Tim de Groot
@@ -267,6 +267,9 @@ class ArgumentParser:
267
267
  " • '-r run_start=2024-07-30 15:27:20.184407' -> remove specified run\n"
268
268
  " • '-r alias=some_alias,tag=prod'\n"
269
269
  " • '-r limit=10' -> keep only the 10 most recent runs\n"
270
+ " • '-r age=10d' -> remove runs older than 10 days\n"
271
+ " • '-r age=-10d' -> remove runs younger than 10 days\n"
272
+ " • (y)ear/(d)ay/(h)our/(m)inute/(s)econd supported\n"
270
273
  ),
271
274
  action="append",
272
275
  nargs="*",
@@ -193,6 +193,68 @@
193
193
  min-width: 60px;
194
194
  }
195
195
 
196
+ /* Add custom section horizontal tile — shown in customize-view mode (unified grid only) */
197
+ .add-section-tile {
198
+ cursor: pointer;
199
+ display: flex;
200
+ align-items: center;
201
+ justify-content: center;
202
+ border: 2px dashed var(--bs-border-color, rgba(128,128,128,0.4));
203
+ border-radius: 8px;
204
+ transition: border-color 0.15s, background-color 0.15s;
205
+ }
206
+
207
+ .add-section-tile:hover {
208
+ border-color: var(--color-text);
209
+ background-color: rgba(128,128,128,0.08);
210
+ }
211
+
212
+ .add-section-tile-inner {
213
+ display: flex;
214
+ flex-direction: row;
215
+ align-items: center;
216
+ gap: 0.5rem;
217
+ user-select: none;
218
+ }
219
+
220
+ .add-section-plus {
221
+ font-size: 1.5rem;
222
+ font-weight: 300;
223
+ line-height: 1;
224
+ }
225
+
226
+ .add-section-tile-label {
227
+ font-size: 0.85rem;
228
+ font-weight: 500;
229
+ }
230
+
231
+ /* Custom section divider bar rendered inside the unified GridStack */
232
+ .custom-section-divider {
233
+ display: flex;
234
+ align-items: center;
235
+ padding: 0 1rem;
236
+ border-radius: 6px;
237
+ height: 100%;
238
+ width: 100%;
239
+ position: relative;
240
+ }
241
+
242
+ .custom-section-title {
243
+ flex: 1;
244
+ font-size: 1rem;
245
+ font-weight: 600;
246
+ text-align: center;
247
+ user-select: none;
248
+ }
249
+
250
+ /* Delete button on custom section dividers — pinned to top-right */
251
+ .delete-custom-section {
252
+ position: absolute;
253
+ top: 5px;
254
+ right: 5px;
255
+ z-index: 1;
256
+ }
257
+
196
258
  /* Delete button on custom stat widgets — pinned to top-right, out of flex flow */
197
259
  .delete-custom-stat-widget {
198
260
  position: absolute;
@@ -1,9 +1,10 @@
1
1
  import sqlite3
2
+ import re
2
3
  from pathlib import Path
3
4
  from .queries import *
4
5
  from .abstractdb import AbstractDatabaseProcessor
5
6
  from time import time
6
- from datetime import datetime, timezone
7
+ from datetime import datetime, timezone, timedelta
7
8
  from typing import Union
8
9
 
9
10
  # Explicit adapter for datetime -> ISO string, replacing the deprecated default
@@ -399,6 +400,8 @@ class DatabaseProcessor(AbstractDatabaseProcessor):
399
400
  console += self._remove_by_tag(run, run_starts, run_tags)
400
401
  elif "limit=" in run:
401
402
  console += self._remove_by_limit(run, run_starts)
403
+ elif "age=" in run:
404
+ console += self._remove_by_age(run, run_starts)
402
405
  else:
403
406
  print(
404
407
  f" ERROR: incorrect usage of the remove_run feature ({run}), check out robotdashboard --help for instructions"
@@ -490,6 +493,37 @@ class DatabaseProcessor(AbstractDatabaseProcessor):
490
493
  console += f" Removed run from the database: index={index}, run_start={run_starts[index]}\n"
491
494
  return console
492
495
 
496
+ def _remove_by_age(self, run_query: str, run_starts: list):
497
+ console = ""
498
+ try:
499
+ clean_query = run_query.replace("age=", "")
500
+ mod, delta = self.parse_time_range(clean_query)
501
+ except ValueError as e:
502
+ return f" ERROR: {e}"
503
+ cutoff = datetime.now(timezone.utc)-delta
504
+ targets = []
505
+ for r in run_starts:
506
+ try:
507
+ run_dt = datetime.fromisoformat(r)
508
+ if run_dt.tzinfo is None:
509
+ run_dt = run_dt.replace(tzinfo=timezone.utc)
510
+ if mod == "+":
511
+ if run_dt < cutoff:
512
+ targets.append(r)
513
+ elif mod == "-":
514
+ if run_dt > cutoff:
515
+ targets.append(r)
516
+ except ValueError as e:
517
+ print(f" WARNING: Skipping invalid timestamp: '{r}' ({e})")
518
+ if not targets:
519
+ console += f" WARNING: no runs were removed as no runs were within range {clean_query}"
520
+ return console
521
+ for run_to_remove in targets:
522
+ self._remove_run(run_to_remove)
523
+ print(f" Removed run from the database: run_start={run_to_remove}")
524
+ console += f" Removed run from the database: run_start={run_to_remove}\n"
525
+ return console
526
+
493
527
  def _remove_run(self, run_start: str):
494
528
  """Helper function to remove the data from all tables"""
495
529
  self.connection.cursor().execute(DELETE_FROM_RUNS.format(run_start=run_start))
@@ -510,6 +544,25 @@ class DatabaseProcessor(AbstractDatabaseProcessor):
510
544
  print(f" Vacuumed the database in {round(end - start, 2)} seconds")
511
545
  return console
512
546
 
547
+ def parse_time_range(self, range_str: str):
548
+ # Regex groups : [modifier] [value] [unit]
549
+ # e.g., +10d, -4h, 1y
550
+ match = re.match(r"([+-])?(\d+)([smhdy])", range_str)
551
+ if not match:
552
+ raise ValueError("Invalid format. Use e.g., 10d, +5h, -1y")
553
+ modifier, value, unit = match.groups()
554
+ value = int(value)
555
+ units = {
556
+ 's': 'seconds',
557
+ 'm': 'minutes',
558
+ 'h': 'hours',
559
+ 'd': 'days',
560
+ 'y': 'days'
561
+ }
562
+ # assume year is 365 days
563
+ delta_kwargs = {units[unit]: value * (365 if unit == 'y' else 1)}
564
+ return modifier or '+', timedelta(**delta_kwargs)
565
+
513
566
  def update_output_path(self, log_path: str):
514
567
  """Function to update the output_path using the log path that the server has used"""
515
568
  console = ""
@@ -116,6 +116,8 @@ function remove_outputs() {
116
116
  const removeTags = document.getElementById("removeTags").value.split(",")
117
117
  const removeLimit = document.getElementById("removeLimit").value
118
118
  if (removeLimit != "") { data["limit"] = removeLimit }
119
+ const removeAge = document.getElementById("removeAge").value
120
+ if (removeAge != "") { data["age"] = removeAge }
119
121
  for (const removeTag of removeTags) {
120
122
  if (removeTag == "") { continue }
121
123
  tags.push(removeTag)
@@ -126,6 +128,7 @@ function remove_outputs() {
126
128
  document.getElementById("removeAliases").value = ""
127
129
  document.getElementById("removeTags").value = ""
128
130
  document.getElementById("removeLimit").value = ""
131
+ document.getElementById("removeAge").value = ""
129
132
  const body = JSON.stringify(data)
130
133
  send_request("DELETE", "/remove-outputs", body, "removeSpinner")
131
134
  }
@@ -0,0 +1,224 @@
1
+ import { settings } from './variables/settings.js';
2
+ import { set_local_storage_item } from './localstorage.js';
3
+ import { STAT_WIDGET_COLORS, STAT_WIDGET_BG_COLORS } from './variables/statwidgetdefs.js';
4
+
5
+ // Generates a short random ID (safe across all modern browsers)
6
+ function generate_section_id() {
7
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
8
+ return crypto.randomUUID().replace(/-/g, '').slice(0, 12);
9
+ }
10
+ return Math.random().toString(36).slice(2, 14);
11
+ }
12
+
13
+ // Applies (or updates) the bg class on the grid-stack-item-content of a section item el
14
+ function apply_section_bg_class(itemEl, bgColor) {
15
+ const content = itemEl.querySelector('.grid-stack-item-content');
16
+ if (!content) return;
17
+ content.classList.remove('blue-bg', 'green-bg', 'red-bg', 'yellow-bg');
18
+ if (bgColor) content.classList.add(bgColor);
19
+ }
20
+
21
+ // Builds the inner HTML for a custom section divider bar
22
+ function build_section_divider_html(section, editMode) {
23
+ const deleteBtn = editMode
24
+ ? `<button type="button" class="btn-close btn-close-sm delete-custom-section" aria-label="Remove section" data-section-id="${section.id}"></button>`
25
+ : '';
26
+ return `<div class="custom-section-divider ${section.textColor || 'white-text'}">
27
+ <span class="custom-section-title">${section.title}</span>
28
+ ${deleteBtn}
29
+ </div>`;
30
+ }
31
+
32
+ // Adds all custom section dividers (for unified grid) to the provided GridStack
33
+ function render_custom_sections(gridStack, editMode) {
34
+ const savedLayout = settings.layouts?.['gridUnified']
35
+ ? JSON.parse(settings.layouts['gridUnified'])
36
+ : null;
37
+
38
+ const sections = settings.customSections || [];
39
+ for (const section of sections) {
40
+ const saved = savedLayout?.find(l => l.id === `customSection-${section.id}`);
41
+
42
+ const item = document.createElement('div');
43
+ item.classList.add('grid-stack-item');
44
+ item.setAttribute('gs-w', saved ? saved.w : 12);
45
+ item.setAttribute('gs-h', saved ? saved.h : 1);
46
+ item.setAttribute('gs-min-w', 12);
47
+ item.setAttribute('gs-max-w', 12);
48
+ item.setAttribute('gs-min-h', 1);
49
+ item.setAttribute('gs-max-h', 3);
50
+ if (saved) {
51
+ item.setAttribute('gs-x', saved.x);
52
+ item.setAttribute('gs-y', saved.y);
53
+ }
54
+ item.setAttribute('data-gs-id', `customSection-${section.id}`);
55
+ item.innerHTML = `<div class="grid-stack-item-content">${build_section_divider_html(section, editMode)}</div>`;
56
+ gridStack.makeWidget(item);
57
+ apply_section_bg_class(item, section.bgColor);
58
+ }
59
+ }
60
+
61
+ // Adds a special "Add custom section" horizontal tile to the unified GridStack for edit mode
62
+ function render_add_section_tile(gridStack) {
63
+ const tileId = 'addSectionTile-unified';
64
+ if (gridStack.el.querySelector(`[data-gs-id="${tileId}"]`)) return;
65
+
66
+ const item = document.createElement('div');
67
+ item.classList.add('grid-stack-item');
68
+ item.setAttribute('gs-w', 12);
69
+ item.setAttribute('gs-h', 1);
70
+ item.setAttribute('gs-min-w', 12);
71
+ item.setAttribute('gs-max-w', 12);
72
+ item.setAttribute('gs-min-h', 1);
73
+ item.setAttribute('gs-max-h', 1);
74
+ item.setAttribute('gs-no-resize', 'true');
75
+ item.setAttribute('data-gs-id', tileId);
76
+ item.innerHTML = `<div class="grid-stack-item-content add-section-tile">
77
+ <div class="add-section-tile-inner">
78
+ <div class="add-section-plus">+</div>
79
+ <div class="add-section-tile-label">Add custom section</div>
80
+ </div>
81
+ </div>`;
82
+ gridStack.makeWidget(item);
83
+ item.querySelector('.add-section-tile').addEventListener('click', () => {
84
+ open_add_custom_section_modal();
85
+ });
86
+ }
87
+
88
+ // Saves a new custom section divider to localStorage and returns it
89
+ function add_custom_section(title, bgColor, textColor) {
90
+ const id = generate_section_id();
91
+ const section = { id, title, bgColor, textColor };
92
+ const list = settings.customSections ? [...settings.customSections] : [];
93
+ list.push(section);
94
+ set_local_storage_item('customSections', list);
95
+ return section;
96
+ }
97
+
98
+ // Removes a custom section divider from localStorage by its id
99
+ function remove_custom_section(id) {
100
+ const list = (settings.customSections || []).filter(s => s.id !== id);
101
+ set_local_storage_item('customSections', list);
102
+ }
103
+
104
+ // Populates the background color picker in the Add Custom Section modal
105
+ function populate_section_bg_colors() {
106
+ const picker = document.getElementById('addCustomSectionBgColorPicker');
107
+ if (!picker) return;
108
+ picker.innerHTML = '';
109
+ for (const c of STAT_WIDGET_BG_COLORS) {
110
+ const btn = document.createElement('button');
111
+ btn.type = 'button';
112
+ btn.className = 'btn btn-outline-light btn-sm stat-color-btn';
113
+ btn.dataset.color = c.value;
114
+ btn.textContent = c.label;
115
+ if (c.value === '') btn.classList.add('active');
116
+ btn.addEventListener('click', () => {
117
+ picker.querySelectorAll('.stat-color-btn').forEach(b => b.classList.remove('active'));
118
+ btn.classList.add('active');
119
+ });
120
+ picker.appendChild(btn);
121
+ }
122
+ }
123
+
124
+ // Populates the text color picker in the Add Custom Section modal
125
+ function populate_section_text_colors() {
126
+ const picker = document.getElementById('addCustomSectionTextColorPicker');
127
+ if (!picker) return;
128
+ picker.innerHTML = '';
129
+ for (const c of STAT_WIDGET_COLORS) {
130
+ const btn = document.createElement('button');
131
+ btn.type = 'button';
132
+ btn.className = 'btn btn-outline-light btn-sm stat-color-btn';
133
+ btn.dataset.color = c.value;
134
+ btn.textContent = c.label;
135
+ if (c.value === 'white-text') btn.classList.add('active');
136
+ btn.addEventListener('click', () => {
137
+ picker.querySelectorAll('.stat-color-btn').forEach(b => b.classList.remove('active'));
138
+ btn.classList.add('active');
139
+ });
140
+ picker.appendChild(btn);
141
+ }
142
+ }
143
+
144
+ // Wires all events inside the Add Custom Section modal (call once after DOM ready)
145
+ function setup_add_custom_section_modal() {
146
+ populate_section_bg_colors();
147
+ populate_section_text_colors();
148
+
149
+ document.getElementById('addCustomSectionConfirm')?.addEventListener('click', () => {
150
+ const titleInput = document.getElementById('addCustomSectionTitle');
151
+ const title = titleInput?.value.trim() || 'Section';
152
+ const bgColorBtn = document.querySelector('#addCustomSectionBgColorPicker .stat-color-btn.active');
153
+ const bgColor = bgColorBtn?.dataset.color || '';
154
+ const textColorBtn = document.querySelector('#addCustomSectionTextColorPicker .stat-color-btn.active');
155
+ const textColor = textColorBtn?.dataset.color || 'white-text';
156
+
157
+ const section = add_custom_section(title, bgColor, textColor);
158
+
159
+ const grid = window['gridUnified'];
160
+ if (grid) {
161
+ const item = document.createElement('div');
162
+ item.classList.add('grid-stack-item');
163
+ item.setAttribute('gs-w', 12);
164
+ item.setAttribute('gs-h', 1);
165
+ item.setAttribute('gs-min-w', 12);
166
+ item.setAttribute('gs-max-w', 12);
167
+ item.setAttribute('gs-min-h', 1);
168
+ item.setAttribute('gs-max-h', 3);
169
+ item.setAttribute('data-gs-id', `customSection-${section.id}`);
170
+ item.innerHTML = `<div class="grid-stack-item-content">${build_section_divider_html(section, true)}</div>`;
171
+ grid.makeWidget(item);
172
+ apply_section_bg_class(item, section.bgColor);
173
+ const deleteBtn = item.querySelector(`.delete-custom-section[data-section-id="${section.id}"]`);
174
+ if (deleteBtn) {
175
+ deleteBtn.addEventListener('click', () => handle_delete_section(section.id, grid));
176
+ }
177
+ }
178
+
179
+ document.dispatchEvent(new CustomEvent("layout-user-action"));
180
+
181
+ if (titleInput) titleInput.value = '';
182
+ bootstrap.Modal.getInstance(document.getElementById('addCustomSectionModal'))?.hide();
183
+ });
184
+
185
+ // Reset color pickers each time the modal opens
186
+ document.getElementById('addCustomSectionModal')?.addEventListener('show.bs.modal', () => {
187
+ populate_section_bg_colors();
188
+ populate_section_text_colors();
189
+ });
190
+ }
191
+
192
+ // Wires delete buttons on all currently rendered custom section dividers
193
+ function wire_delete_section_buttons(gridStack) {
194
+ const sections = settings.customSections || [];
195
+ for (const section of sections) {
196
+ const btn = document.querySelector(`.delete-custom-section[data-section-id="${section.id}"]`);
197
+ if (btn) {
198
+ btn.addEventListener('click', () => handle_delete_section(section.id, gridStack));
199
+ }
200
+ }
201
+ }
202
+
203
+ function handle_delete_section(id, gridStack) {
204
+ const el = gridStack?.el?.querySelector(`[data-gs-id="customSection-${id}"]`);
205
+ if (el && gridStack) {
206
+ gridStack.removeWidget(el);
207
+ }
208
+ remove_custom_section(id);
209
+ document.dispatchEvent(new CustomEvent("layout-user-action"));
210
+ }
211
+
212
+ // Opens the Add Custom Section modal
213
+ function open_add_custom_section_modal() {
214
+ const modal = document.getElementById('addCustomSectionModal');
215
+ if (!modal) return;
216
+ bootstrap.Modal.getOrCreateInstance(modal).show();
217
+ }
218
+
219
+ export {
220
+ render_custom_sections,
221
+ render_add_section_tile,
222
+ setup_add_custom_section_modal,
223
+ wire_delete_section_buttons,
224
+ };
@@ -26,6 +26,7 @@ import {
26
26
  setup_lowest_highest_dates,
27
27
  clear_all_filters,
28
28
  setup_project_versions_in_select_filter_buttons,
29
+ setup_suite_path_navigator,
29
30
  setup_custom_filters_in_select_filter_buttons,
30
31
  update_overview_version_select_list,
31
32
  setup_metadata_filter,
@@ -251,6 +252,7 @@ function setup_filter_modal() {
251
252
  setup_runs_in_select_filter_buttons();
252
253
  setup_runtags_in_select_filter_buttons();
253
254
  setup_project_versions_in_select_filter_buttons();
255
+ setup_suite_path_navigator("All");
254
256
  setup_custom_filters_in_select_filter_buttons();
255
257
  // snapshot the default/initial filter state so profile checkboxes can reflect changes
256
258
  capture_default_filters();
@@ -822,9 +824,16 @@ function setup_sections_filters() {
822
824
  update_overview_filter_visibility();
823
825
  });
824
826
  document.getElementById("suiteSelectSuites").addEventListener("change", () => {
825
- update_graphs_with_loading(["suiteStatisticsGraph", "suiteDurationGraph"], () => {
827
+ const mostGraphIds = settings.switch.sectionFiltersApplySuite
828
+ ? ["suiteMostFailedGraph", "suiteMostTimeConsumingGraph"]
829
+ : [];
830
+ update_graphs_with_loading(["suiteStatisticsGraph", "suiteDurationGraph", ...mostGraphIds], () => {
826
831
  update_suite_duration_graph();
827
832
  update_suite_statistics_graph();
833
+ if (settings.switch.sectionFiltersApplySuite) {
834
+ update_suite_most_failed_graph();
835
+ update_suite_most_time_consuming_graph();
836
+ }
828
837
  });
829
838
  });
830
839
  update_switch_local_storage("switch.suitePathsSuiteSection", settings.switch.suitePathsSuiteSection, true);
@@ -842,20 +851,44 @@ function setup_sections_filters() {
842
851
  }
843
852
  );
844
853
  });
854
+ update_switch_local_storage("switch.sectionFiltersApplySuite", settings.switch.sectionFiltersApplySuite, true);
855
+ document.getElementById("switchSectionFiltersApplySuite").addEventListener("change", () => {
856
+ settings.switch.sectionFiltersApplySuite = !settings.switch.sectionFiltersApplySuite;
857
+ update_switch_local_storage("switch.sectionFiltersApplySuite", settings.switch.sectionFiltersApplySuite);
858
+ update_graphs_with_loading(
859
+ ["suiteMostFailedGraph", "suiteMostTimeConsumingGraph"],
860
+ () => {
861
+ update_suite_most_failed_graph();
862
+ update_suite_most_time_consuming_graph();
863
+ }
864
+ );
865
+ });
845
866
  document.getElementById("resetSuiteFolder").addEventListener("click", () => {
846
867
  update_graphs_with_loading(["suiteFolderDonutGraph", "suiteFolderFailDonutGraph", "suiteStatisticsGraph", "suiteDurationGraph"], () => {
847
868
  update_suite_folder_donut_graph("");
848
869
  });
849
870
  });
850
871
  document.getElementById("suiteSelectTests").addEventListener("change", () => {
872
+ const mostGraphIds = settings.switch.sectionFiltersApplyTest
873
+ ? ["testMessagesGraph", "testMostFlakyGraph", "testRecentMostFlakyGraph",
874
+ "testMostFailedGraph", "testRecentMostFailedGraph", "testMostTimeConsumingGraph"]
875
+ : [];
851
876
  update_graphs_with_loading(
852
- ["testStatisticsGraph", "testDurationGraph", "testDurationDeviationGraph"],
877
+ ["testStatisticsGraph", "testDurationGraph", "testDurationDeviationGraph", ...mostGraphIds],
853
878
  () => {
854
879
  setup_testtags_in_select();
855
880
  setup_tests_in_select();
856
881
  update_test_statistics_graph();
857
882
  update_test_duration_graph();
858
883
  update_test_duration_deviation_graph();
884
+ if (settings.switch.sectionFiltersApplyTest) {
885
+ update_test_messages_graph();
886
+ update_test_most_flaky_graph();
887
+ update_test_recent_most_flaky_graph();
888
+ update_test_most_failed_graph();
889
+ update_test_recent_most_failed_graph();
890
+ update_test_most_time_consuming_graph();
891
+ }
859
892
  }
860
893
  );
861
894
  });
@@ -881,31 +914,75 @@ function setup_sections_filters() {
881
914
  }
882
915
  );
883
916
  });
917
+ update_switch_local_storage("switch.sectionFiltersApplyTest", settings.switch.sectionFiltersApplyTest, true);
918
+ document.getElementById("switchSectionFiltersApplyTest").addEventListener("change", () => {
919
+ settings.switch.sectionFiltersApplyTest = !settings.switch.sectionFiltersApplyTest;
920
+ update_switch_local_storage("switch.sectionFiltersApplyTest", settings.switch.sectionFiltersApplyTest);
921
+ update_graphs_with_loading(
922
+ ["testMessagesGraph", "testMostFlakyGraph", "testRecentMostFlakyGraph",
923
+ "testMostFailedGraph", "testRecentMostFailedGraph", "testMostTimeConsumingGraph"],
924
+ () => {
925
+ update_test_messages_graph();
926
+ update_test_most_flaky_graph();
927
+ update_test_recent_most_flaky_graph();
928
+ update_test_most_failed_graph();
929
+ update_test_recent_most_failed_graph();
930
+ update_test_most_time_consuming_graph();
931
+ }
932
+ );
933
+ });
884
934
  document.getElementById("testTagsSelect").addEventListener("change", () => {
935
+ const mostGraphIds = settings.switch.sectionFiltersApplyTest
936
+ ? ["testMessagesGraph", "testMostFlakyGraph", "testRecentMostFlakyGraph",
937
+ "testMostFailedGraph", "testRecentMostFailedGraph", "testMostTimeConsumingGraph"]
938
+ : [];
885
939
  update_graphs_with_loading(
886
- ["testStatisticsGraph", "testDurationGraph", "testDurationDeviationGraph"],
940
+ ["testStatisticsGraph", "testDurationGraph", "testDurationDeviationGraph", ...mostGraphIds],
887
941
  () => {
888
942
  setup_tests_in_select();
889
943
  update_test_statistics_graph();
890
944
  update_test_duration_graph();
891
945
  update_test_duration_deviation_graph();
946
+ if (settings.switch.sectionFiltersApplyTest) {
947
+ update_test_messages_graph();
948
+ update_test_most_flaky_graph();
949
+ update_test_recent_most_flaky_graph();
950
+ update_test_most_failed_graph();
951
+ update_test_recent_most_failed_graph();
952
+ update_test_most_time_consuming_graph();
953
+ }
892
954
  }
893
955
  );
894
956
  });
895
957
  document.getElementById("testSelect").addEventListener("change", () => {
958
+ const mostGraphIds = settings.switch.sectionFiltersApplyTest
959
+ ? ["testMessagesGraph", "testMostFlakyGraph", "testRecentMostFlakyGraph",
960
+ "testMostFailedGraph", "testRecentMostFailedGraph", "testMostTimeConsumingGraph"]
961
+ : [];
896
962
  update_graphs_with_loading(
897
- ["testStatisticsGraph", "testDurationGraph", "testDurationDeviationGraph"],
963
+ ["testStatisticsGraph", "testDurationGraph", "testDurationDeviationGraph", ...mostGraphIds],
898
964
  () => {
899
965
  update_test_statistics_graph();
900
966
  update_test_duration_graph();
901
967
  update_test_duration_deviation_graph();
968
+ if (settings.switch.sectionFiltersApplyTest) {
969
+ update_test_messages_graph();
970
+ update_test_most_flaky_graph();
971
+ update_test_recent_most_flaky_graph();
972
+ update_test_most_failed_graph();
973
+ update_test_recent_most_failed_graph();
974
+ update_test_most_time_consuming_graph();
975
+ }
902
976
  }
903
977
  );
904
978
  });
905
979
  document.getElementById("keywordSelect").addEventListener("change", () => {
980
+ const mostGraphIds = settings.switch.sectionFiltersApplyKeyword
981
+ ? ["keywordMostFailedGraph", "keywordMostTimeConsumingGraph", "keywordMostUsedGraph"]
982
+ : [];
906
983
  update_graphs_with_loading(
907
984
  ["keywordStatisticsGraph", "keywordTimesRunGraph", "keywordTotalDurationGraph",
908
- "keywordAverageDurationGraph", "keywordMinDurationGraph", "keywordMaxDurationGraph"],
985
+ "keywordAverageDurationGraph", "keywordMinDurationGraph", "keywordMaxDurationGraph", ...mostGraphIds],
909
986
  () => {
910
987
  update_keyword_statistics_graph();
911
988
  update_keyword_times_run_graph();
@@ -913,6 +990,24 @@ function setup_sections_filters() {
913
990
  update_keyword_average_duration_graph();
914
991
  update_keyword_min_duration_graph();
915
992
  update_keyword_max_duration_graph();
993
+ if (settings.switch.sectionFiltersApplyKeyword) {
994
+ update_keyword_most_failed_graph();
995
+ update_keyword_most_time_consuming_graph();
996
+ update_keyword_most_used_graph();
997
+ }
998
+ }
999
+ );
1000
+ });
1001
+ update_switch_local_storage("switch.sectionFiltersApplyKeyword", settings.switch.sectionFiltersApplyKeyword, true);
1002
+ document.getElementById("switchSectionFiltersApplyKeyword").addEventListener("change", () => {
1003
+ settings.switch.sectionFiltersApplyKeyword = !settings.switch.sectionFiltersApplyKeyword;
1004
+ update_switch_local_storage("switch.sectionFiltersApplyKeyword", settings.switch.sectionFiltersApplyKeyword);
1005
+ update_graphs_with_loading(
1006
+ ["keywordMostFailedGraph", "keywordMostTimeConsumingGraph", "keywordMostUsedGraph"],
1007
+ () => {
1008
+ update_keyword_most_failed_graph();
1009
+ update_keyword_most_time_consuming_graph();
1010
+ update_keyword_most_used_graph();
916
1011
  }
917
1012
  );
918
1013
  });