robotframework-dashboard 2.1.0__tar.gz → 2.3.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 (111) hide show
  1. {robotframework_dashboard-2.1.0/robotframework_dashboard.egg-info → robotframework_dashboard-2.3.0}/PKG-INFO +4 -3
  2. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/README.md +5 -1
  3. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/arguments.py +91 -4
  4. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/css/base.css +16 -2
  5. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/css/components.css +191 -72
  6. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/css/dark.css +0 -1
  7. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/dashboard.py +7 -1
  8. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/database.py +146 -22
  9. robotframework_dashboard-2.3.0/robotframework_dashboard/dependencies/bootstrap.css +6 -0
  10. robotframework_dashboard-2.3.0/robotframework_dashboard/dependencies/bootstrap.js +7 -0
  11. robotframework_dashboard-2.3.0/robotframework_dashboard/dependencies/chart.js +14 -0
  12. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/dependencies/chartjs-adapter-date-fns.js +1 -1
  13. robotframework_dashboard-2.3.0/robotframework_dashboard/dependencies/chartjs-chart-boxplot.js +2 -0
  14. robotframework_dashboard-2.3.0/robotframework_dashboard/dependencies/chartjs-chart-matrix.js +8 -0
  15. robotframework_dashboard-2.3.0/robotframework_dashboard/dependencies/chartjs-plugin-datalabels.js +7 -0
  16. robotframework_dashboard-2.3.0/robotframework_dashboard/dependencies/datatables.css +21 -0
  17. robotframework_dashboard-2.3.0/robotframework_dashboard/dependencies/datatables.js +22 -0
  18. robotframework_dashboard-2.3.0/robotframework_dashboard/dependencies/gridstack.css +1 -0
  19. robotframework_dashboard-2.3.0/robotframework_dashboard/dependencies/gridstack.js +9 -0
  20. robotframework_dashboard-2.3.0/robotframework_dashboard/dependencies/pako.js +2 -0
  21. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/dependencies.py +29 -25
  22. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/admin_page/admin_api.js +21 -3
  23. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/admin_page/admin_eventlisteners.js +0 -1
  24. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/admin_page/admin_information.js +1 -1
  25. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/common.js +133 -11
  26. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/customsections.js +12 -86
  27. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/eventlisteners.js +50 -62
  28. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/filter.js +52 -157
  29. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_creation/all.js +14 -3
  30. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_creation/chart_factory.js +0 -2
  31. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_creation/compare.js +2 -4
  32. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_creation/config_helpers.js +4 -5
  33. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_creation/keyword.js +84 -4
  34. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_creation/overview.js +95 -224
  35. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_creation/run.js +0 -3
  36. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_creation/suite.js +0 -4
  37. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_creation/tables.js +19 -6
  38. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_creation/test.js +3 -4
  39. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/duration.js +0 -2
  40. robotframework_dashboard-2.3.0/robotframework_dashboard/js/graph_data/exceptions.js +94 -0
  41. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/failed.js +7 -2
  42. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/flaky.js +30 -49
  43. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/graph_config.js +6 -1
  44. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/helpers.js +80 -10
  45. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/messages.js +6 -2
  46. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/statistics.js +40 -21
  47. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/stats.js +6 -0
  48. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/tooltip_helpers.js +0 -1
  49. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/information.js +1 -1
  50. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/layout.js +110 -28
  51. robotframework_dashboard-2.3.0/robotframework_dashboard/js/linkwidgets.js +181 -0
  52. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/localstorage.js +6 -10
  53. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/log.js +8 -1
  54. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/menu.js +10 -40
  55. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/statwidgets.js +154 -84
  56. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/theme.js +36 -10
  57. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/variables/chartconfig.js +5 -0
  58. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/variables/data.js +3 -1
  59. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/variables/globals.js +2 -2
  60. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/variables/graphmetadata.js +43 -7
  61. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/variables/information.js +18 -5
  62. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/variables/settings.js +3 -3
  63. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/variables/statwidgetdefs.js +3 -0
  64. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/variables/svg.js +16 -0
  65. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/licenses/THIRD_PARTY_LICENSES.txt +65 -56
  66. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/main.py +1 -0
  67. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/processors.py +123 -12
  68. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/queries.py +9 -2
  69. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/robotdashboard.py +18 -1
  70. robotframework_dashboard-2.3.0/robotframework_dashboard/robotdashboardlistener.py +321 -0
  71. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/server.py +76 -12
  72. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/templates/admin.html +2 -2
  73. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/templates/dashboard.html +153 -29
  74. robotframework_dashboard-2.3.0/robotframework_dashboard/version.py +1 -0
  75. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0/robotframework_dashboard.egg-info}/PKG-INFO +4 -3
  76. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard.egg-info/SOURCES.txt +3 -0
  77. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/setup.py +4 -3
  78. robotframework_dashboard-2.1.0/robotframework_dashboard/dependencies/bootstrap.css +0 -6
  79. robotframework_dashboard-2.1.0/robotframework_dashboard/dependencies/bootstrap.js +0 -7
  80. robotframework_dashboard-2.1.0/robotframework_dashboard/dependencies/chart.js +0 -20
  81. robotframework_dashboard-2.1.0/robotframework_dashboard/dependencies/chartjs-chart-boxplot.js +0 -2
  82. robotframework_dashboard-2.1.0/robotframework_dashboard/dependencies/chartjs-chart-matrix.js +0 -8
  83. robotframework_dashboard-2.1.0/robotframework_dashboard/dependencies/chartjs-plugin-datalabels.js +0 -7
  84. robotframework_dashboard-2.1.0/robotframework_dashboard/dependencies/datatables.css +0 -18
  85. robotframework_dashboard-2.1.0/robotframework_dashboard/dependencies/datatables.js +0 -25
  86. robotframework_dashboard-2.1.0/robotframework_dashboard/dependencies/gridstack.css +0 -1
  87. robotframework_dashboard-2.1.0/robotframework_dashboard/dependencies/gridstack.js +0 -9
  88. robotframework_dashboard-2.1.0/robotframework_dashboard/dependencies/pako.js +0 -2
  89. robotframework_dashboard-2.1.0/robotframework_dashboard/version.py +0 -1
  90. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/LICENSE +0 -0
  91. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/MANIFEST.in +0 -0
  92. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/pyproject.toml +0 -0
  93. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/__init__.py +0 -0
  94. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/abstractdb.py +0 -0
  95. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/css/colors.css +0 -0
  96. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/admin_page/admin_common.js +0 -0
  97. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/admin_page/admin_main.js +0 -0
  98. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/admin_page/admin_theme.js +0 -0
  99. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/database.js +0 -0
  100. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/donut.js +0 -0
  101. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/duration_deviation.js +0 -0
  102. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/heatmap.js +0 -0
  103. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/graph_data/time_consuming.js +0 -0
  104. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/main.js +0 -0
  105. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard/js/variables/graphs.js +0 -0
  106. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard.egg-info/dependency_links.txt +0 -0
  107. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard.egg-info/entry_points.txt +0 -0
  108. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard.egg-info/not-zip-safe +0 -0
  109. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard.egg-info/requires.txt +0 -0
  110. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/robotframework_dashboard.egg-info/top_level.txt +0 -0
  111. {robotframework_dashboard-2.1.0 → robotframework_dashboard-2.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-dashboard
3
- Version: 2.1.0
3
+ Version: 2.3.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
@@ -90,8 +90,9 @@ For all available CLI options see the [Basic CLI docs](https://marketsquare.gith
90
90
  - 🛠️ **Custom Database Classes** - Extend or replace the database processor for custom backends.
91
91
  - 🖥️ **Server Mode** - Host your dashboard for multi-user access and automatic updates.
92
92
  - 🎧 **Listener Integration** - Automatically updates dashboard after every test run.
93
- - 📝 **Message Config Support** - Group similar test failures using regex-based patterns.
94
- - ⚙️ **Configurable Defaults** - Preload dashboard settings via JSON for consistent appearance.
93
+ - 📝 **Message Config Support** - Group similar test failures using regex-based patterns.
94
+ - 🚨 **Exception Tracking** - Track exception messages caught by TRY/EXCEPT blocks across runs, with a dedicated graph and table.
95
+ - ⚙️ **Configurable Defaults** - Preload dashboard settings via JSON for consistent appearance.
95
96
 
96
97
  …and many more advanced features to help you visualize, analyze, and manage your Robot Framework test results with ease!
97
98
 
@@ -57,10 +57,12 @@ For all available CLI options see the [Basic CLI docs](https://marketsquare.gith
57
57
  - 🔎 **Global Filters** - Filter runs by name, tags, date, metadata, or quantity.
58
58
  - ⚖️ **Comparison Mode** - Compare up to 4 runs side by side with visual statistics.
59
59
  - 🔗 **Automatic Log Linking** - Open Robot Framework logs directly from the dashboard.
60
+ - 🔁 **Rerun History** - Merged `--rerunfailed` outputs keep the status of every attempt per test.
60
61
  - 🛠️ **Custom Database Classes** - Extend or replace the database processor for custom backends.
61
62
  - 🖥️ **Server Mode** - Host your dashboard for multi-user access and automatic updates.
62
63
  - 🎧 **Listener Integration** - Automatically updates dashboard after every test run.
63
64
  - 📝 **Message Config Support** - Group similar test failures using regex-based patterns.
65
+ - 🚨 **Exception Tracking** - Track exception messages caught by TRY/EXCEPT blocks across runs, with a dedicated graph and table.
64
66
  - ⚙️ **Configurable Defaults** - Preload dashboard settings via JSON for consistent appearance.
65
67
 
66
68
  …and many more advanced features to help you visualize, analyze, and manage your Robot Framework test results with ease!
@@ -78,10 +80,12 @@ For detailed usage instructions, advanced examples, and full documentation, visi
78
80
  - 🔍 [**Filtering**](https://marketsquare.github.io/robotframework-dashboard/filtering.html) - Apply filters to analyze trends in your test data and highlight specific tags, amounts or datetime ranges.
79
81
  - 🎨 [**Customization**](https://marketsquare.github.io/robotframework-dashboard/customization.html) - Customize dashboard sections, graph layouts, and visualizations to suit your workflow.
80
82
  - ⚙️ [**Settings**](https://marketsquare.github.io/robotframework-dashboard/settings.html) - Configure dashboard preferences including themes, default views, graph options, and save your settings for consistent team-wide use.
83
+ - 📐 [**Architecture**](https://marketsquare.github.io/robotframework-dashboard/architecture.html) - See how robotdashboard is structured internally, from CLI invocation to a rendered dashboard, in both Regular Mode and Server Mode.
81
84
  - 🖥️ [**Dashboard Server**](https://marketsquare.github.io/robotframework-dashboard/dashboard-server.html) - Host the dashboard for multi-user access, programmatic updates, and remote server integration.
82
85
  - 🗄️ [**Custom Database Class**](https://marketsquare.github.io/robotframework-dashboard/custom-database-class.html) - Extend or replace the default database backend to suit your storage needs, including SQLite, MySQL, or custom implementations.
83
86
  - 🔔 [**Listener Integration**](https://marketsquare.github.io/robotframework-dashboard/listener-integration.html) - Use a listener to automatically push test results to the dashboard for every executed run, integrating seamlessly into CI/CD pipelines.
84
87
  - 📂 [**Log Linking**](https://marketsquare.github.io/robotframework-dashboard/log-linking.html) - Enable clickable log navigation from dashboard graphs, covering file naming conventions, local and server usage, and remote log uploads.
88
+ - 🔁 [**Reruns**](https://marketsquare.github.io/robotframework-dashboard/reruns.html) - Feed merged `--rerunfailed` outputs to the dashboard and see the attempt history of every re-executed test.
85
89
  - 📈 [**Performance**](https://marketsquare.github.io/robotframework-dashboard/performance.html) - Performance results across different workload sizes, guidance on scaling to hundreds of runs, and tips for large result sets.
86
90
 
87
91
 
@@ -122,4 +126,4 @@ This project is licensed under the MIT License.
122
126
  ## 🎬 RoboCon Talk
123
127
  See also my 2025 [RoboCon](https://robocon.io) talk, where I explain robotdashboard and its use cases. Note that the UI has improved significantly since then.
124
128
 
125
- [![Robocon 2025 talk](https://img.youtube.com/vi/olf1_pd9YfM/0.jpg)](https://www.youtube.com/watch?v=olf1_pd9YfM)
129
+ [![Robocon 2025 talk](https://img.youtube.com/vi/olf1_pd9YfM/0.jpg)](https://www.youtube.com/watch?v=olf1_pd9YfM)
@@ -4,8 +4,16 @@ from sys import exit
4
4
  from re import split
5
5
  from os import getcwd
6
6
  from os.path import join, exists
7
+ from typing import NamedTuple
7
8
  from .version import __version__
8
9
 
10
+ _VALID_LOG_TYPES = {"run", "suite", "test", "keyword", "all"}
11
+
12
+
13
+ class LogRemovedConfig(NamedTuple):
14
+ types: list
15
+ path: str
16
+
9
17
 
10
18
  class dotdict(dict):
11
19
  """dot.notation access to dictionary attributes"""
@@ -61,6 +69,46 @@ class ArgumentParser:
61
69
  print(" ERROR: Mixing --projectversion and version_ tags not supported")
62
70
  exit(2)
63
71
 
72
+ def _process_remove_runs(self, parts):
73
+ """Process and validate the comma-separated --removeruns parts.
74
+
75
+ Detects a scoped-retention combination ('limit' + 'tags') and rewrites
76
+ it into the internal 'limit=N;tag=...' form so the limit acts on the
77
+ tagged subset instead of running as two independent operations. This
78
+ lets users write the combo with the regular comma separator (e.g.
79
+ '-r "limit=10,tag=nightly"') instead of ';'.
80
+
81
+ Only 'limit' can be scoped by tags; 'age' + 'tags' run as two
82
+ independent operations.
83
+
84
+ Rules:
85
+ - No tags, or tags without a 'limit' partner -> no combination, every
86
+ part stays independent (unchanged behavior, including 'age'+'tags').
87
+ - Only one 'limit' may be combined with tags (error otherwise).
88
+ """
89
+ limit_parts = [p for p in parts if p.startswith("limit=")]
90
+ tag_parts = [p for p in parts if p.startswith("tag=")]
91
+ other_parts = [
92
+ p for p in parts if not (p.startswith("limit=") or p.startswith("tag="))
93
+ ]
94
+
95
+ if not tag_parts or not limit_parts:
96
+ return parts
97
+ if len(limit_parts) > 1:
98
+ print(" ERROR: Only one 'limit' may be combined with 'tag(s)'.")
99
+ exit(3)
100
+
101
+ tag_values = [p.replace("tag=", "") for p in tag_parts]
102
+ limit_value = limit_parts[0].replace("limit=", "")
103
+ combo = limit_parts[0] + "".join(f";tag={value}" for value in tag_values)
104
+ print(
105
+ f" INFO: Combining 'limit' with 'tag(s)' -> keeping the {limit_value} most recent "
106
+ f"run(s) tagged with [{', '.join(tag_values)}] and removing older matching runs "
107
+ f"(runs without these tags are left untouched)."
108
+ )
109
+ # other independent options (index/run_start/alias) run before the combo
110
+ return other_parts + [combo]
111
+
64
112
  def _check_argument_warnings(self, arguments, outputs, outputfolderpaths, use_logs, generate_dashboard, no_autoupdate, offline_dependencies):
65
113
  """Checks for argument combinations that are valid but likely unintended and prints warnings"""
66
114
  no_outputs = not outputs and not outputfolderpaths
@@ -267,6 +315,7 @@ class ArgumentParser:
267
315
  " • '-r run_start=2024-07-30 15:27:20.184407' -> remove specified run\n"
268
316
  " • '-r alias=some_alias,tag=prod'\n"
269
317
  " • '-r limit=10' -> keep only the 10 most recent runs\n"
318
+ " • '-r limit=10,tag=nightly' -> keep 10 newest 'nightly' runs, leave others\n"
270
319
  " • '-r age=10d' -> remove runs older than 10 days\n"
271
320
  " • '-r age=-10d' -> remove runs younger than 10 days\n"
272
321
  " • (y)ear/(d)ay/(h)our/(m)inute/(s)econd supported\n"
@@ -275,6 +324,24 @@ class ArgumentParser:
275
324
  nargs="*",
276
325
  default=None,
277
326
  )
327
+ db_group.add_argument(
328
+ "--logremoved",
329
+ metavar="TYPES_PATH",
330
+ help=(
331
+ "Log (append if file exists) run data to jsonl before removing from the db\n"
332
+ " • Supply what types to log (default=all) and (optional) a path to the desired .jsonl\n"
333
+ "Examples:\n"
334
+ " • '--logremoved \"/tmp/removed_runs.jsonl\"'\n"
335
+ " • '--logremoved \"run:suite\"'\n"
336
+ " • '--logremoved \"all:/tmp/removed_runs.jsonl\"'\n"
337
+ ),
338
+ action="store",
339
+ type=str,
340
+ nargs="?",
341
+ const="all",
342
+ default=None,
343
+ dest="logremoved",
344
+ )
278
345
  db_group.add_argument(
279
346
  "-c",
280
347
  "--databaseclass",
@@ -505,11 +572,13 @@ class ArgumentParser:
505
572
  # handles the processing of --removeruns
506
573
  remove_runs = None
507
574
  if arguments.removeruns:
508
- remove_runs = []
575
+ raw_parts = []
509
576
  for runs in arguments.removeruns:
510
- parts = str(runs[0]).split(",")
511
- for part in parts:
512
- remove_runs.append(part)
577
+ for part in str(runs[0]).split(","):
578
+ part = part.strip()
579
+ if part:
580
+ raw_parts.append(part)
581
+ remove_runs = self._process_remove_runs(raw_parts)
513
582
 
514
583
  # handles the boolean handling of relevant arguments
515
584
  generate_dashboard = self._normalize_bool(
@@ -605,6 +674,23 @@ class ArgumentParser:
605
674
  ssl_certfile = arguments.ssl_certfile
606
675
  ssl_keyfile = arguments.ssl_keyfile
607
676
 
677
+ # handles --logremoved: "type1:type2:path" or "path" or "type1:type2"
678
+ log_removed = None
679
+ if arguments.logremoved:
680
+ parts = arguments.logremoved.split(":")
681
+ types, remaining = [], list(parts)
682
+ # grab element types, leave path, for windows drive letter X: parse
683
+ for part in parts:
684
+ if part in _VALID_LOG_TYPES:
685
+ types.append(part)
686
+ remaining.pop(0)
687
+ else:
688
+ break
689
+ if not types:
690
+ types = ["all"]
691
+ path = ":".join(remaining) if remaining else "robot_removed_runs.jsonl"
692
+ log_removed = LogRemovedConfig(types=types, path=path)
693
+
608
694
  # validates argument combinations
609
695
  self._check_argument_errors(arguments, outputs, outputfolderpaths, force_json_config, database_class)
610
696
  self._check_argument_warnings(arguments, outputs, outputfolderpaths, use_logs, generate_dashboard, no_autoupdate, offline_dependencies)
@@ -640,5 +726,6 @@ class ArgumentParser:
640
726
  "ssl_certfile": ssl_certfile,
641
727
  "ssl_keyfile": ssl_keyfile,
642
728
  "log_url": arguments.logurl,
729
+ "log_removed": log_removed,
643
730
  }
644
731
  return dotdict(provided_args)
@@ -124,6 +124,14 @@ body {
124
124
  .boxplot-graph:hover svg,
125
125
  .shown-graph:hover svg,
126
126
  .hidden-graph:hover svg,
127
+ .move-to-first-graph:hover svg,
128
+ .move-to-last-graph:hover svg,
129
+ .add-stat-widget-header:hover svg,
130
+ .add-link-widget-header:hover svg,
131
+ .add-section-header:hover svg,
132
+ .delete-custom-stat-widget:hover svg,
133
+ .delete-custom-link-widget:hover svg,
134
+ .delete-custom-section:hover svg,
127
135
  .shown-section:hover svg,
128
136
  .hidden-section:hover svg,
129
137
  .collapse-icon:hover svg,
@@ -188,6 +196,14 @@ body.lock-scroll {
188
196
  .boxplot-graph,
189
197
  .shown-graph,
190
198
  .hidden-graph,
199
+ .move-to-first-graph,
200
+ .move-to-last-graph,
201
+ .add-stat-widget-header,
202
+ .add-link-widget-header,
203
+ .add-section-header,
204
+ .delete-custom-stat-widget,
205
+ .delete-custom-link-widget,
206
+ .delete-custom-section,
191
207
  .shown-section,
192
208
  .hidden-section,
193
209
  .move-up-table,
@@ -197,7 +213,6 @@ body.lock-scroll {
197
213
  cursor: pointer;
198
214
  }
199
215
 
200
- /* ── Info label + small icon (tooltip on label hover) ── */
201
216
  .info-label {
202
217
  display: inline-flex !important;
203
218
  align-items: center;
@@ -267,7 +282,6 @@ body.lock-scroll {
267
282
  display: block;
268
283
  }
269
284
 
270
- /* ── Sidebar (off-canvas menu for overflowed items) ── */
271
285
  .sidenav-backdrop {
272
286
  position: fixed;
273
287
  top: 0; left: 0; right: 0; bottom: 0;
@@ -37,7 +37,7 @@
37
37
  flex-wrap: wrap;
38
38
  align-items: center;
39
39
  column-gap: 0.75rem;
40
- row-gap: 0.35rem;
40
+ row-gap: 6px;
41
41
  }
42
42
 
43
43
  .section-filters .filter-group {
@@ -115,6 +115,37 @@
115
115
  display: block;
116
116
  }
117
117
 
118
+ .filter-search-wrapper {
119
+ position: relative;
120
+ }
121
+
122
+ .filter-search-wrapper .form-control {
123
+ padding-right: 1.5rem;
124
+ }
125
+
126
+ .filter-search-clear {
127
+ position: absolute;
128
+ top: 0;
129
+ right: 0;
130
+ bottom: 0;
131
+ width: 1.5rem;
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ padding: 0 0 0.15rem 0;
136
+ border: none;
137
+ background: none;
138
+ color: inherit;
139
+ opacity: 0.6;
140
+ font-size: 1.4rem;
141
+ line-height: 1;
142
+ cursor: pointer;
143
+ }
144
+
145
+ .filter-search-clear:hover {
146
+ opacity: 1;
147
+ }
148
+
118
149
  .filter-profile-bar {
119
150
  border-bottom: 1px solid var(--bs-border-color);
120
151
  }
@@ -152,80 +183,44 @@
152
183
  align-items: center;
153
184
  }
154
185
 
155
- /* Add Widget tile shown in customize-view mode */
156
- .add-stat-widget-tile {
157
- cursor: pointer;
158
- display: flex;
159
- align-items: center;
160
- justify-content: center;
161
- border: 2px dashed var(--bs-border-color, rgba(128,128,128,0.4));
162
- border-radius: 8px;
163
- transition: border-color 0.15s, background-color 0.15s;
186
+ /* Color picker buttons inside the Add Stat Widget modal */
187
+ .stat-color-btn {
188
+ min-width: 60px;
164
189
  }
165
190
 
166
- .add-stat-widget-tile:hover {
167
- border-color: var(--color-text);
168
- background-color: rgba(128,128,128,0.08);
191
+ /* "All" tab stat list inside the Add Stat Widget modal */
192
+ .add-all-widgets-list {
193
+ max-height: 60vh;
194
+ overflow-y: auto;
169
195
  }
170
196
 
171
- .add-stat-widget-tile-inner {
197
+ .add-all-widgets-row {
172
198
  display: flex;
173
- flex-direction: column;
174
199
  align-items: center;
175
- gap: 0.25rem;
176
- transition: opacity 0.15s;
177
- user-select: none;
178
- }
179
-
180
- .add-stat-widget-plus {
181
- font-size: 2rem;
182
- font-weight: 300;
183
- line-height: 1;
184
- }
185
-
186
- .add-stat-widget-tile-label {
187
- font-size: 0.8rem;
188
- font-weight: 500;
189
- }
190
-
191
- /* Color picker buttons inside the Add Stat Widget modal */
192
- .stat-color-btn {
193
- min-width: 60px;
200
+ gap: 0.5rem;
201
+ margin-bottom: 0.5rem;
194
202
  }
195
203
 
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;
204
+ .add-all-widgets-row .form-check {
205
+ margin-bottom: 0;
205
206
  }
206
207
 
207
- .add-section-tile:hover {
208
- border-color: var(--color-text);
209
- background-color: rgba(128,128,128,0.08);
208
+ .add-all-widgets-row .add-all-widgets-title {
209
+ flex: 1 1 auto;
210
210
  }
211
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;
212
+ .add-all-widgets-row .add-all-widgets-badge {
213
+ flex: 0 0 auto;
218
214
  }
219
215
 
220
- .add-section-plus {
221
- font-size: 1.5rem;
222
- font-weight: 300;
223
- line-height: 1;
216
+ .add-all-widgets-group-heading {
217
+ font-weight: 600;
218
+ margin-top: 0.5rem;
219
+ margin-bottom: 0.25rem;
224
220
  }
225
221
 
226
- .add-section-tile-label {
227
- font-size: 0.85rem;
228
- font-weight: 500;
222
+ .add-all-widgets-group-heading:first-child {
223
+ margin-top: 0;
229
224
  }
230
225
 
231
226
  /* Custom section divider bar rendered inside the unified GridStack */
@@ -247,20 +242,16 @@
247
242
  user-select: none;
248
243
  }
249
244
 
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
-
258
- /* Delete button on custom stat widgets — pinned to top-right, out of flex flow */
259
- .delete-custom-stat-widget {
245
+ /* Move to first/last + delete controls on custom widgets/sections — pinned to top-right,
246
+ out of flex flow, sized and spaced like the icons in graph headers */
247
+ .custom-widget-move-controls {
260
248
  position: absolute;
261
249
  top: 5px;
262
250
  right: 5px;
263
251
  z-index: 1;
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 8px;
264
255
  }
265
256
 
266
257
  /* Compact flex layout for custom stat widgets — works at any GridStack size */
@@ -284,7 +275,12 @@
284
275
  overflow: hidden;
285
276
  text-overflow: ellipsis;
286
277
  white-space: nowrap;
287
- max-width: calc(100% - 20px); /* leave room so text doesn't run under the delete button */
278
+ max-width: 100%;
279
+ }
280
+
281
+ /* Leave room for the move/delete controls pinned to the top-right in edit mode */
282
+ .stat-widget-inner:has(.custom-widget-move-controls) .stat-widget-title {
283
+ max-width: calc(100% - 96px);
288
284
  }
289
285
 
290
286
  /* When the item is very short (h=1 row ≈ 60px) switch to a side-by-side row */
@@ -304,14 +300,25 @@
304
300
  .grid-stack-item[data-gs-h="1"] .stat-widget-title {
305
301
  flex: 1 1 0;
306
302
  text-align: left;
307
- max-width: calc(100% - 40px); /* value + delete button */
303
+ max-width: calc(100% - 40px); /* leave room for the value */
304
+ }
305
+
306
+ /* Leave room for the move/delete controls pinned to the top-right in edit mode */
307
+ .grid-stack-item[gs-h="1"] .stat-widget-inner:has(.custom-widget-move-controls) .stat-widget-title,
308
+ .grid-stack-item[data-gs-h="1"] .stat-widget-inner:has(.custom-widget-move-controls) .stat-widget-title {
309
+ max-width: calc(100% - 136px); /* value + move/delete controls */
308
310
  }
309
311
 
310
312
  .grid-stack-item[gs-h="1"] .stat-value,
311
313
  .grid-stack-item[data-gs-h="1"] .stat-value {
312
314
  flex-shrink: 0;
313
315
  font-size: 1.1rem;
314
- margin-right: 20px; /* keep clear of the absolute delete button */
316
+ }
317
+
318
+ /* Leave room for the move/delete controls pinned to the top-right in edit mode */
319
+ .grid-stack-item[gs-h="1"] .stat-widget-inner:has(.custom-widget-move-controls) .stat-value,
320
+ .grid-stack-item[data-gs-h="1"] .stat-widget-inner:has(.custom-widget-move-controls) .stat-value {
321
+ margin-right: 96px;
315
322
  }
316
323
 
317
324
  .grid-stack-item-content.blue-bg { background-color: rgba(13, 110, 253, 0.15); }
@@ -319,6 +326,88 @@
319
326
  .grid-stack-item-content.red-bg { background-color: rgba(206, 62, 1, 0.2); }
320
327
  .grid-stack-item-content.yellow-bg { background-color: rgba(254, 216, 79, 0.2); }
321
328
 
329
+ /* Inner layout — mirrors stat-widget-inner */
330
+ .link-widget-inner {
331
+ position: relative;
332
+ display: flex;
333
+ flex-direction: column;
334
+ justify-content: center;
335
+ align-items: center;
336
+ gap: 0.25rem;
337
+ height: 100%;
338
+ text-align: center;
339
+ overflow: hidden;
340
+ }
341
+
342
+ /* Label — main large text, like .stat-value */
343
+ .link-widget-label {
344
+ margin: 0;
345
+ font-weight: 600;
346
+ font-size: 1.5rem;
347
+ line-height: 1.2;
348
+ overflow: hidden;
349
+ text-overflow: ellipsis;
350
+ white-space: nowrap;
351
+ max-width: 100%;
352
+ }
353
+
354
+ /* URL preview — small muted text below the label */
355
+ .link-widget-url {
356
+ font-size: 0.72rem;
357
+ opacity: 0.55;
358
+ overflow: hidden;
359
+ text-overflow: ellipsis;
360
+ white-space: nowrap;
361
+ max-width: 100%;
362
+ }
363
+
364
+ /* Leave room for the move/delete controls pinned to the top-right in edit mode */
365
+ .link-widget-inner:has(.custom-widget-move-controls) .link-widget-label,
366
+ .link-widget-inner:has(.custom-widget-move-controls) .link-widget-url {
367
+ max-width: calc(100% - 96px);
368
+ }
369
+
370
+ /* Clickable in non-edit mode */
371
+ .link-widget-clickable {
372
+ cursor: pointer;
373
+ }
374
+
375
+ /* Same padding as stat widget tiles */
376
+ .grid-stack-item-content:has(.link-widget-inner) {
377
+ padding: 8px 12px;
378
+ }
379
+
380
+ /* h=1 compact row — mirrors stat widget compact layout */
381
+ .grid-stack-item[gs-h="1"] .link-widget-inner,
382
+ .grid-stack-item[data-gs-h="1"] .link-widget-inner {
383
+ flex-direction: row;
384
+ justify-content: space-between;
385
+ align-items: center;
386
+ gap: 0.5rem;
387
+ }
388
+
389
+ .grid-stack-item[gs-h="1"] .link-widget-label,
390
+ .grid-stack-item[data-gs-h="1"] .link-widget-label {
391
+ flex: 1 1 0;
392
+ font-size: 1.1rem;
393
+ text-align: left;
394
+ max-width: calc(60% - 10px);
395
+ }
396
+
397
+ .grid-stack-item[gs-h="1"] .link-widget-url,
398
+ .grid-stack-item[data-gs-h="1"] .link-widget-url {
399
+ flex-shrink: 0;
400
+ font-size: 0.72rem;
401
+ text-align: right;
402
+ max-width: 40%;
403
+ }
404
+
405
+ /* Leave room for the move/delete controls pinned to the top-right in edit mode */
406
+ .grid-stack-item[gs-h="1"] .link-widget-inner:has(.custom-widget-move-controls) .link-widget-url {
407
+ max-width: calc(40% - 96px);
408
+ margin-right: 96px;
409
+ }
410
+
322
411
  .fullscreen .stat-value {
323
412
  font-size: 4rem;
324
413
  }
@@ -361,6 +450,22 @@
361
450
  height: 200px;
362
451
  }
363
452
 
453
+ /* relative run time, pinned to the bottom right corner of a run card without
454
+ affecting the layout of the stats/duration columns next to the donut */
455
+ .overview-card .card-body {
456
+ position: relative;
457
+ }
458
+
459
+ .run-card-run-time {
460
+ position: absolute;
461
+ right: 1rem;
462
+ bottom: 0.55rem;
463
+ color: var(--color-text-muted);
464
+ cursor: help;
465
+ font-size: 0.85em;
466
+ white-space: nowrap;
467
+ }
468
+
364
469
  .overview-card {
365
470
  cursor: pointer;
366
471
  min-width: 300px;
@@ -425,6 +530,13 @@
425
530
  gap: 8px;
426
531
  }
427
532
 
533
+ /* Icon row in section card headers (add stat/link widget, add section, move/show/hide section) */
534
+ .section-header-icons {
535
+ display: flex;
536
+ align-items: center;
537
+ gap: 8px;
538
+ }
539
+
428
540
  .grid-stack-item-content .graph-body {
429
541
  flex: 1 1 auto;
430
542
  display: flex;
@@ -472,6 +584,13 @@ canvas {
472
584
  filter: brightness(0.5);
473
585
  }
474
586
 
587
+ /* Bootstrap >= 5.3.1 no longer spaces the modal header itself (only its .btn-close gets
588
+ margin-left: auto); the dashboard modals use their own button groups, so keep the
589
+ title left and the buttons right */
590
+ .modal-header {
591
+ justify-content: space-between;
592
+ }
593
+
475
594
  #runTag {
476
595
  max-height: 400px;
477
596
  overflow: auto;
@@ -1,5 +1,4 @@
1
1
  .modal-dialog {
2
- background: var(--color-modal-bg);
3
2
  color: var(--color-text);
4
3
  }
5
4
 
@@ -68,6 +68,10 @@ class DashboardGenerator:
68
68
  '"placeholder_keywords"',
69
69
  f'"{self._compress_and_encode(data["keywords"])}"',
70
70
  )
71
+ dashboard_data = dashboard_data.replace(
72
+ '"placeholder_exceptions"',
73
+ f'"{self._compress_and_encode(data.get("exceptions", []))}"',
74
+ )
71
75
  dashboard_data = dashboard_data.replace(
72
76
  '"placeholder_amount"', str(quantity)
73
77
  )
@@ -93,9 +97,11 @@ class DashboardGenerator:
93
97
  f"Robot Framework Dashboard - {str(generation_datetime)[:-7]}",
94
98
  )
95
99
  if message_config:
100
+ # the placeholder sits inside a single-quoted JS string literal that is
101
+ # JSON.parse'd at runtime, so escape backslashes and single quotes
96
102
  dashboard_data = dashboard_data.replace(
97
103
  '"placeholder_message_config"',
98
- str(message_config).replace("'", '"'),
104
+ dumps(message_config).replace("\\", "\\\\").replace("'", "\\'"),
99
105
  )
100
106
  if json_config:
101
107
  dashboard_data = dashboard_data.replace(