solid_queue_monitor 1.3.0 → 2.0.0
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.
- checksums.yaml +4 -4
- data/README.md +20 -4
- data/app/assets/javascripts/solid_queue_monitor/application.js +393 -0
- data/app/{services/solid_queue_monitor/stylesheet_generator.rb → assets/stylesheets/solid_queue_monitor/application.css} +23 -12
- data/app/controllers/solid_queue_monitor/application_controller.rb +2 -2
- data/app/controllers/solid_queue_monitor/assets_controller.rb +52 -0
- data/app/controllers/solid_queue_monitor/base_controller.rb +0 -29
- data/app/controllers/solid_queue_monitor/failed_jobs_controller.rb +3 -7
- data/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb +3 -6
- data/app/controllers/solid_queue_monitor/jobs_controller.rb +3 -7
- data/app/controllers/solid_queue_monitor/overview_controller.rb +3 -12
- data/app/controllers/solid_queue_monitor/queues_controller.rb +4 -18
- data/app/controllers/solid_queue_monitor/ready_jobs_controller.rb +3 -6
- data/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb +3 -6
- data/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb +3 -7
- data/app/controllers/solid_queue_monitor/search_controller.rb +3 -4
- data/app/controllers/solid_queue_monitor/workers_controller.rb +24 -8
- data/app/helpers/solid_queue_monitor/application_helper.rb +46 -0
- data/app/helpers/solid_queue_monitor/chart_helper.rb +293 -0
- data/app/helpers/solid_queue_monitor/job_details_helper.rb +66 -0
- data/app/helpers/solid_queue_monitor/jobs_helper.rb +134 -0
- data/app/helpers/solid_queue_monitor/pagination_helper.rb +23 -0
- data/app/helpers/solid_queue_monitor/sort_helper.rb +30 -0
- data/app/helpers/solid_queue_monitor/workers_helper.rb +88 -0
- data/app/services/solid_queue_monitor/asset_cache.rb +56 -0
- data/app/views/layouts/solid_queue_monitor/application.html.erb +25 -0
- data/app/views/solid_queue_monitor/failed_jobs/_row.html.erb +26 -0
- data/app/views/solid_queue_monitor/failed_jobs/index.html.erb +38 -0
- data/app/views/solid_queue_monitor/in_progress_jobs/_row.html.erb +13 -0
- data/app/views/solid_queue_monitor/in_progress_jobs/index.html.erb +25 -0
- data/app/views/solid_queue_monitor/jobs/_arguments.html.erb +9 -0
- data/app/views/solid_queue_monitor/jobs/_error.html.erb +26 -0
- data/app/views/solid_queue_monitor/jobs/_execution_history.html.erb +25 -0
- data/app/views/solid_queue_monitor/jobs/_header.html.erb +37 -0
- data/app/views/solid_queue_monitor/jobs/_metadata.html.erb +22 -0
- data/app/views/solid_queue_monitor/jobs/_raw_data.html.erb +11 -0
- data/app/views/solid_queue_monitor/jobs/_timeline.html.erb +29 -0
- data/app/views/solid_queue_monitor/jobs/_timing.html.erb +9 -0
- data/app/views/solid_queue_monitor/jobs/_worker.html.erb +12 -0
- data/app/views/solid_queue_monitor/jobs/show.html.erb +22 -0
- data/app/views/solid_queue_monitor/overview/_chart.html.erb +1 -0
- data/app/views/solid_queue_monitor/overview/_recent_job_row.html.erb +26 -0
- data/app/views/solid_queue_monitor/overview/_recent_jobs.html.erb +31 -0
- data/app/views/solid_queue_monitor/overview/_stat_card.html.erb +4 -0
- data/app/views/solid_queue_monitor/overview/_stats.html.erb +11 -0
- data/app/views/solid_queue_monitor/overview/index.html.erb +9 -0
- data/app/views/solid_queue_monitor/queues/_job_row.html.erb +26 -0
- data/app/views/solid_queue_monitor/queues/_row.html.erb +33 -0
- data/app/views/solid_queue_monitor/queues/index.html.erb +18 -0
- data/app/views/solid_queue_monitor/queues/show.html.erb +63 -0
- data/app/views/solid_queue_monitor/ready_jobs/_row.html.erb +7 -0
- data/app/views/solid_queue_monitor/ready_jobs/index.html.erb +25 -0
- data/app/views/solid_queue_monitor/recurring_jobs/_row.html.erb +8 -0
- data/app/views/solid_queue_monitor/recurring_jobs/index.html.erb +26 -0
- data/app/views/solid_queue_monitor/scheduled_jobs/_row.html.erb +7 -0
- data/app/views/solid_queue_monitor/scheduled_jobs/index.html.erb +36 -0
- data/app/views/solid_queue_monitor/search/_completed_row.html.erb +6 -0
- data/app/views/solid_queue_monitor/search/_failed_row.html.erb +6 -0
- data/app/views/solid_queue_monitor/search/_job_row.html.erb +9 -0
- data/app/views/solid_queue_monitor/search/_recurring_row.html.erb +6 -0
- data/app/views/solid_queue_monitor/search/_section.html.erb +25 -0
- data/app/views/solid_queue_monitor/search/index.html.erb +23 -0
- data/app/views/solid_queue_monitor/shared/_filters.html.erb +48 -0
- data/app/views/solid_queue_monitor/shared/_flash.html.erb +17 -0
- data/app/views/solid_queue_monitor/shared/_footer.html.erb +3 -0
- data/app/views/solid_queue_monitor/shared/_header.html.erb +81 -0
- data/app/views/solid_queue_monitor/shared/_jobs_table.html.erb +20 -0
- data/app/views/solid_queue_monitor/shared/_pagination.html.erb +25 -0
- data/app/views/solid_queue_monitor/workers/_row.html.erb +22 -0
- data/app/views/solid_queue_monitor/workers/index.html.erb +82 -0
- data/config/routes.rb +6 -1
- data/lib/solid_queue_monitor/engine.rb +2 -0
- data/lib/solid_queue_monitor/version.rb +1 -1
- metadata +57 -17
- data/app/presenters/solid_queue_monitor/base_presenter.rb +0 -211
- data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +0 -225
- data/app/presenters/solid_queue_monitor/in_progress_jobs_presenter.rb +0 -84
- data/app/presenters/solid_queue_monitor/job_details_presenter.rb +0 -707
- data/app/presenters/solid_queue_monitor/jobs_presenter.rb +0 -144
- data/app/presenters/solid_queue_monitor/queue_details_presenter.rb +0 -195
- data/app/presenters/solid_queue_monitor/queues_presenter.rb +0 -89
- data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +0 -81
- data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +0 -81
- data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +0 -178
- data/app/presenters/solid_queue_monitor/search_results_presenter.rb +0 -190
- data/app/presenters/solid_queue_monitor/stats_presenter.rb +0 -36
- data/app/presenters/solid_queue_monitor/workers_presenter.rb +0 -325
- data/app/services/solid_queue_monitor/chart_presenter.rb +0 -239
- data/app/services/solid_queue_monitor/html_generator.rb +0 -427
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SolidQueueMonitor
|
|
4
|
-
class HtmlGenerator
|
|
5
|
-
include Rails.application.routes.url_helpers
|
|
6
|
-
include SolidQueueMonitor::Engine.routes.url_helpers
|
|
7
|
-
|
|
8
|
-
def initialize(title:, content:, message: nil, message_type: nil, search_query: nil, nonce: nil)
|
|
9
|
-
@title = title
|
|
10
|
-
@content = content
|
|
11
|
-
@message = message
|
|
12
|
-
@message_type = message_type
|
|
13
|
-
@search_query = search_query
|
|
14
|
-
@nonce = nonce
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def generate
|
|
18
|
-
<<-HTML
|
|
19
|
-
<!DOCTYPE html>
|
|
20
|
-
<html>
|
|
21
|
-
<head>
|
|
22
|
-
<title>Solid Queue Monitor - #{@title}</title>
|
|
23
|
-
#{generate_head}
|
|
24
|
-
</head>
|
|
25
|
-
<body class="solid_queue_monitor">
|
|
26
|
-
#{generate_body}
|
|
27
|
-
</body>
|
|
28
|
-
</html>
|
|
29
|
-
HTML
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def generate_head
|
|
35
|
-
<<-HTML
|
|
36
|
-
<meta charset="UTF-8">
|
|
37
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
38
|
-
#{style_tag_open}
|
|
39
|
-
#{SolidQueueMonitor::StylesheetGenerator.new.generate}
|
|
40
|
-
</style>
|
|
41
|
-
HTML
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def generate_body
|
|
45
|
-
<<-HTML
|
|
46
|
-
#{render_message}
|
|
47
|
-
<div class="container">
|
|
48
|
-
#{generate_header}
|
|
49
|
-
<div class="section">
|
|
50
|
-
<h2>#{@title}</h2>
|
|
51
|
-
#{@content}
|
|
52
|
-
</div>
|
|
53
|
-
#{generate_footer}
|
|
54
|
-
</div>
|
|
55
|
-
#{generate_auto_refresh_script}
|
|
56
|
-
#{generate_chart_script}
|
|
57
|
-
HTML
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def render_message
|
|
61
|
-
return '' unless @message
|
|
62
|
-
|
|
63
|
-
class_name = @message_type == 'success' ? 'message-success' : 'message-error'
|
|
64
|
-
<<-HTML
|
|
65
|
-
<div id="flash-message" class="message #{class_name}">#{@message}</div>
|
|
66
|
-
#{script_tag_open}
|
|
67
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
68
|
-
var el = document.getElementById('flash-message');
|
|
69
|
-
if (!el) return;
|
|
70
|
-
setTimeout(function() {
|
|
71
|
-
el.classList.add('is-fading');
|
|
72
|
-
setTimeout(function() { el.classList.add('is-hidden'); }, 500);
|
|
73
|
-
}, 5000);
|
|
74
|
-
});
|
|
75
|
-
</script>
|
|
76
|
-
HTML
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def generate_header
|
|
80
|
-
nav_items = [
|
|
81
|
-
{ path: root_path, label: 'Overview', match: 'Overview' },
|
|
82
|
-
{ path: ready_jobs_path, label: 'Ready Jobs', match: 'Ready Jobs' },
|
|
83
|
-
{ path: in_progress_jobs_path, label: 'In Progress Jobs', match: 'In Progress' },
|
|
84
|
-
{ path: scheduled_jobs_path, label: 'Scheduled Jobs', match: 'Scheduled Jobs' },
|
|
85
|
-
{ path: recurring_jobs_path, label: 'Recurring Jobs', match: 'Recurring Jobs' },
|
|
86
|
-
{ path: failed_jobs_path, label: 'Failed Jobs', match: 'Failed Jobs' },
|
|
87
|
-
{ path: queues_path, label: 'Queues', match: 'Queues' },
|
|
88
|
-
{ path: workers_path, label: 'Workers', match: 'Workers' }
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
nav_links = nav_items.map do |item|
|
|
92
|
-
active_class = @title&.include?(item[:match]) ? 'active' : ''
|
|
93
|
-
"<a href=\"#{item[:path]}\" class=\"nav-link #{active_class}\">#{item[:label]}</a>"
|
|
94
|
-
end.join("\n ")
|
|
95
|
-
|
|
96
|
-
<<-HTML
|
|
97
|
-
<header>
|
|
98
|
-
<div class="header-top">
|
|
99
|
-
<h1><a href="#{root_path}" class="header-title-link">Solid Queue Monitor</a></h1>
|
|
100
|
-
#{generate_search_box}
|
|
101
|
-
<div class="header-controls">
|
|
102
|
-
#{generate_auto_refresh_controls}
|
|
103
|
-
#{generate_theme_toggle}
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
<nav class="navigation">
|
|
107
|
-
#{nav_links}
|
|
108
|
-
</nav>
|
|
109
|
-
</header>
|
|
110
|
-
HTML
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def generate_footer
|
|
114
|
-
<<-HTML
|
|
115
|
-
<footer>
|
|
116
|
-
<p>Powered by Solid Queue Monitor</p>
|
|
117
|
-
</footer>
|
|
118
|
-
HTML
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def generate_search_box
|
|
122
|
-
search_value = @search_query ? escape_html(@search_query) : ''
|
|
123
|
-
<<-HTML
|
|
124
|
-
<form method="get" action="#{search_path}" class="header-search-form">
|
|
125
|
-
<input type="text" name="q" value="#{search_value}" placeholder="Search by class, queue, job ID, or error..." class="header-search-input">
|
|
126
|
-
<button type="submit" class="header-search-button" title="Search">
|
|
127
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
128
|
-
<circle cx="11" cy="11" r="8"></circle>
|
|
129
|
-
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
130
|
-
</svg>
|
|
131
|
-
</button>
|
|
132
|
-
</form>
|
|
133
|
-
HTML
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def escape_html(text)
|
|
137
|
-
text.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def style_tag_open
|
|
141
|
-
@nonce ? %(<style nonce="#{@nonce}">) : '<style>'
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def script_tag_open
|
|
145
|
-
@nonce ? %(<script nonce="#{@nonce}">) : '<script>'
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def generate_auto_refresh_controls
|
|
149
|
-
return '' unless SolidQueueMonitor.auto_refresh_enabled
|
|
150
|
-
|
|
151
|
-
interval = SolidQueueMonitor.auto_refresh_interval
|
|
152
|
-
<<-HTML
|
|
153
|
-
<div class="auto-refresh-container" title="Auto-refresh every #{interval}s" data-tooltip="Auto-refresh: Dashboard updates automatically every #{interval} seconds. Toggle to enable/disable.">
|
|
154
|
-
<span class="auto-refresh-indicator" id="auto-refresh-indicator"></span>
|
|
155
|
-
<span class="auto-refresh-countdown" id="auto-refresh-countdown">#{interval}s</span>
|
|
156
|
-
<label class="auto-refresh-switch" title="Toggle auto-refresh">
|
|
157
|
-
<input type="checkbox" id="auto-refresh-toggle" checked>
|
|
158
|
-
<span class="switch-slider"></span>
|
|
159
|
-
</label>
|
|
160
|
-
<button class="refresh-now-btn" id="refresh-now-btn" title="Refresh now">
|
|
161
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
162
|
-
<path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"/>
|
|
163
|
-
</svg>
|
|
164
|
-
</button>
|
|
165
|
-
</div>
|
|
166
|
-
HTML
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def generate_theme_toggle
|
|
170
|
-
<<-HTML
|
|
171
|
-
<button class="theme-toggle-btn" id="theme-toggle-btn" title="Toggle dark mode">
|
|
172
|
-
<svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
173
|
-
<circle cx="12" cy="12" r="5"></circle>
|
|
174
|
-
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
175
|
-
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
176
|
-
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
177
|
-
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
178
|
-
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
179
|
-
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
180
|
-
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
181
|
-
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
182
|
-
</svg>
|
|
183
|
-
<svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
184
|
-
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
|
185
|
-
</svg>
|
|
186
|
-
</button>
|
|
187
|
-
HTML
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def generate_auto_refresh_script
|
|
191
|
-
return '' unless SolidQueueMonitor.auto_refresh_enabled
|
|
192
|
-
|
|
193
|
-
"#{script_tag_open}#{auto_refresh_javascript}</script>"
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def auto_refresh_javascript
|
|
197
|
-
interval = SolidQueueMonitor.auto_refresh_interval
|
|
198
|
-
<<-JS
|
|
199
|
-
(function() {
|
|
200
|
-
var REFRESH_INTERVAL = #{interval};
|
|
201
|
-
var countdown = REFRESH_INTERVAL;
|
|
202
|
-
var timerId = null;
|
|
203
|
-
var isEnabled = localStorage.getItem('sqm_auto_refresh') !== 'false';
|
|
204
|
-
#{auto_refresh_dom_elements}
|
|
205
|
-
#{auto_refresh_functions}
|
|
206
|
-
#{auto_refresh_event_listeners}
|
|
207
|
-
#{auto_refresh_init}
|
|
208
|
-
})();
|
|
209
|
-
JS
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def auto_refresh_dom_elements
|
|
213
|
-
<<-JS
|
|
214
|
-
var toggle = document.getElementById('auto-refresh-toggle');
|
|
215
|
-
var indicator = document.getElementById('auto-refresh-indicator');
|
|
216
|
-
var countdownEl = document.getElementById('auto-refresh-countdown');
|
|
217
|
-
var refreshBtn = document.getElementById('refresh-now-btn');
|
|
218
|
-
JS
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def auto_refresh_functions
|
|
222
|
-
<<-JS
|
|
223
|
-
function updateUI() {
|
|
224
|
-
if (toggle) toggle.checked = isEnabled;
|
|
225
|
-
if (indicator) indicator.classList.toggle('active', isEnabled);
|
|
226
|
-
if (countdownEl) {
|
|
227
|
-
countdownEl.textContent = countdown + 's';
|
|
228
|
-
countdownEl.classList.toggle('countdown-paused', !isEnabled);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
function tick() {
|
|
232
|
-
countdown--;
|
|
233
|
-
if (countdown <= 0) { refresh(); } else { updateUI(); }
|
|
234
|
-
}
|
|
235
|
-
function startTimer() {
|
|
236
|
-
stopTimer();
|
|
237
|
-
countdown = REFRESH_INTERVAL;
|
|
238
|
-
updateUI();
|
|
239
|
-
timerId = setInterval(tick, 1000);
|
|
240
|
-
}
|
|
241
|
-
function stopTimer() {
|
|
242
|
-
if (timerId) { clearInterval(timerId); timerId = null; }
|
|
243
|
-
}
|
|
244
|
-
function refresh() { window.location.reload(); }
|
|
245
|
-
function setEnabled(enabled) {
|
|
246
|
-
isEnabled = enabled;
|
|
247
|
-
localStorage.setItem('sqm_auto_refresh', enabled ? 'true' : 'false');
|
|
248
|
-
if (enabled) { startTimer(); } else { stopTimer(); countdown = REFRESH_INTERVAL; updateUI(); }
|
|
249
|
-
}
|
|
250
|
-
JS
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def auto_refresh_event_listeners
|
|
254
|
-
<<-JS
|
|
255
|
-
if (toggle) { toggle.addEventListener('change', function() { setEnabled(this.checked); }); }
|
|
256
|
-
if (refreshBtn) { refreshBtn.addEventListener('click', function() { refresh(); }); }
|
|
257
|
-
JS
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def auto_refresh_init
|
|
261
|
-
<<-JS
|
|
262
|
-
updateUI();
|
|
263
|
-
if (isEnabled) { startTimer(); }
|
|
264
|
-
JS
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def generate_chart_script
|
|
268
|
-
<<-HTML
|
|
269
|
-
#{script_tag_open}
|
|
270
|
-
#{theme_toggle_javascript}
|
|
271
|
-
#{chart_tooltip_javascript}
|
|
272
|
-
#{global_behaviors_javascript}
|
|
273
|
-
</script>
|
|
274
|
-
HTML
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def theme_toggle_javascript
|
|
278
|
-
<<-JS
|
|
279
|
-
(function() {
|
|
280
|
-
var body = document.body;
|
|
281
|
-
var themeBtn = document.getElementById('theme-toggle-btn');
|
|
282
|
-
var storageKey = 'sqm_dark_theme';
|
|
283
|
-
|
|
284
|
-
// Check for saved preference or system preference
|
|
285
|
-
function getPreferredTheme() {
|
|
286
|
-
var saved = localStorage.getItem(storageKey);
|
|
287
|
-
if (saved !== null) {
|
|
288
|
-
return saved === 'true';
|
|
289
|
-
}
|
|
290
|
-
// Check system preference
|
|
291
|
-
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function setTheme(isDark) {
|
|
295
|
-
if (isDark) {
|
|
296
|
-
body.classList.add('dark-theme');
|
|
297
|
-
} else {
|
|
298
|
-
body.classList.remove('dark-theme');
|
|
299
|
-
}
|
|
300
|
-
localStorage.setItem(storageKey, isDark ? 'true' : 'false');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Initialize theme
|
|
304
|
-
setTheme(getPreferredTheme());
|
|
305
|
-
|
|
306
|
-
// Toggle on button click
|
|
307
|
-
if (themeBtn) {
|
|
308
|
-
themeBtn.addEventListener('click', function() {
|
|
309
|
-
var isDark = body.classList.contains('dark-theme');
|
|
310
|
-
setTheme(!isDark);
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Listen for system preference changes
|
|
315
|
-
if (window.matchMedia) {
|
|
316
|
-
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
|
317
|
-
// Only auto-switch if user hasn't manually set a preference
|
|
318
|
-
if (localStorage.getItem(storageKey) === null) {
|
|
319
|
-
setTheme(e.matches);
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
})();
|
|
324
|
-
JS
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
def chart_tooltip_javascript
|
|
328
|
-
<<-JS
|
|
329
|
-
(function() {
|
|
330
|
-
// Chart collapse/expand functionality
|
|
331
|
-
var chartSection = document.getElementById('chart-section');
|
|
332
|
-
var toggleBtn = document.getElementById('chart-toggle-btn');
|
|
333
|
-
|
|
334
|
-
if (chartSection && toggleBtn) {
|
|
335
|
-
var isCollapsed = localStorage.getItem('sqm_chart_collapsed') === 'true';
|
|
336
|
-
|
|
337
|
-
if (isCollapsed) {
|
|
338
|
-
chartSection.classList.add('collapsed');
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
toggleBtn.addEventListener('click', function() {
|
|
342
|
-
chartSection.classList.toggle('collapsed');
|
|
343
|
-
var collapsed = chartSection.classList.contains('collapsed');
|
|
344
|
-
localStorage.setItem('sqm_chart_collapsed', collapsed ? 'true' : 'false');
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Chart tooltip functionality
|
|
349
|
-
var tooltip = document.getElementById('chart-tooltip');
|
|
350
|
-
if (!tooltip) return;
|
|
351
|
-
|
|
352
|
-
var dataPoints = document.querySelectorAll('.data-point');
|
|
353
|
-
var seriesNames = { created: 'Created', completed: 'Completed', failed: 'Failed' };
|
|
354
|
-
|
|
355
|
-
dataPoints.forEach(function(point) {
|
|
356
|
-
point.addEventListener('mouseenter', function(e) {
|
|
357
|
-
var series = this.getAttribute('data-series');
|
|
358
|
-
var label = this.getAttribute('data-label');
|
|
359
|
-
var value = this.getAttribute('data-value');
|
|
360
|
-
|
|
361
|
-
tooltip.querySelector('.tooltip-label').textContent = label;
|
|
362
|
-
tooltip.querySelector('.tooltip-value').textContent = seriesNames[series] + ': ' + value;
|
|
363
|
-
tooltip.classList.add('tooltip-visible');
|
|
364
|
-
positionTooltip(e);
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
point.addEventListener('mousemove', function(e) {
|
|
368
|
-
positionTooltip(e);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
point.addEventListener('mouseleave', function() {
|
|
372
|
-
tooltip.classList.remove('tooltip-visible');
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
function positionTooltip(e) {
|
|
377
|
-
var x = e.clientX + 10;
|
|
378
|
-
var y = e.clientY - 30;
|
|
379
|
-
|
|
380
|
-
if (x + tooltip.offsetWidth > window.innerWidth) {
|
|
381
|
-
x = e.clientX - tooltip.offsetWidth - 10;
|
|
382
|
-
}
|
|
383
|
-
if (y < 0) {
|
|
384
|
-
y = e.clientY + 10;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Dynamic cursor-tracked position, not CSP-restricted.
|
|
388
|
-
tooltip.style.left = x + 'px';
|
|
389
|
-
tooltip.style.top = y + 'px';
|
|
390
|
-
}
|
|
391
|
-
})();
|
|
392
|
-
JS
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
def global_behaviors_javascript
|
|
396
|
-
<<-JS
|
|
397
|
-
document.addEventListener('submit', function(e) {
|
|
398
|
-
var form = e.target;
|
|
399
|
-
var msg = form.dataset && form.dataset.confirm;
|
|
400
|
-
if (msg && !window.confirm(msg)) { e.preventDefault(); }
|
|
401
|
-
}, true);
|
|
402
|
-
|
|
403
|
-
document.addEventListener('click', function(e) {
|
|
404
|
-
var el = e.target.closest('[data-confirm-submit]');
|
|
405
|
-
if (!el) return;
|
|
406
|
-
e.preventDefault();
|
|
407
|
-
var msg = el.dataset.confirm || 'Are you sure?';
|
|
408
|
-
if (!window.confirm(msg)) return;
|
|
409
|
-
var formId = el.dataset.confirmSubmit;
|
|
410
|
-
var form = document.getElementById(formId);
|
|
411
|
-
if (form) form.submit();
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
var timeRangeSelect = document.getElementById('chart-time-select');
|
|
415
|
-
if (timeRangeSelect) {
|
|
416
|
-
timeRangeSelect.addEventListener('change', function() {
|
|
417
|
-
window.location.href = '?time_range=' + this.value;
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
JS
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
def default_url_options
|
|
424
|
-
{ only_path: true }
|
|
425
|
-
end
|
|
426
|
-
end
|
|
427
|
-
end
|