solid_log-ui 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +295 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/application.js +6 -0
- data/app/assets/javascripts/solid_log/checkbox_dropdown.js +171 -0
- data/app/assets/javascripts/solid_log/filter_state.js +138 -0
- data/app/assets/javascripts/solid_log/jump_to_live.js +119 -0
- data/app/assets/javascripts/solid_log/live_tail.js +476 -0
- data/app/assets/javascripts/solid_log/live_tail.js.bak +270 -0
- data/app/assets/javascripts/solid_log/log_filters.js +37 -0
- data/app/assets/javascripts/solid_log/stream_scroll.js +195 -0
- data/app/assets/javascripts/solid_log/timeline_histogram.js +162 -0
- data/app/assets/javascripts/solid_log/toast.js +50 -0
- data/app/assets/stylesheets/solid_log/application.css +1329 -0
- data/app/assets/stylesheets/solid_log/components.css +1506 -0
- data/app/assets/stylesheets/solid_log/mission_control.css +398 -0
- data/app/channels/solid_log/ui/application_cable/channel.rb +8 -0
- data/app/channels/solid_log/ui/application_cable/connection.rb +10 -0
- data/app/channels/solid_log/ui/log_stream_channel.rb +132 -0
- data/app/controllers/solid_log/ui/base_controller.rb +122 -0
- data/app/controllers/solid_log/ui/dashboard_controller.rb +32 -0
- data/app/controllers/solid_log/ui/entries_controller.rb +34 -0
- data/app/controllers/solid_log/ui/fields_controller.rb +57 -0
- data/app/controllers/solid_log/ui/streams_controller.rb +204 -0
- data/app/controllers/solid_log/ui/timelines_controller.rb +29 -0
- data/app/controllers/solid_log/ui/tokens_controller.rb +46 -0
- data/app/helpers/solid_log/ui/application_helper.rb +99 -0
- data/app/helpers/solid_log/ui/dashboard_helper.rb +46 -0
- data/app/helpers/solid_log/ui/entries_helper.rb +16 -0
- data/app/helpers/solid_log/ui/timeline_helper.rb +39 -0
- data/app/services/solid_log/ui/live_tail_broadcaster.rb +81 -0
- data/app/views/layouts/solid_log/ui/application.html.erb +53 -0
- data/app/views/solid_log/ui/dashboard/index.html.erb +178 -0
- data/app/views/solid_log/ui/entries/show.html.erb +132 -0
- data/app/views/solid_log/ui/fields/index.html.erb +133 -0
- data/app/views/solid_log/ui/shared/_checkbox_dropdown.html.erb +64 -0
- data/app/views/solid_log/ui/shared/_multiselect_filter.html.erb +37 -0
- data/app/views/solid_log/ui/shared/_toast.html.erb +7 -0
- data/app/views/solid_log/ui/shared/_toast_message.html.erb +30 -0
- data/app/views/solid_log/ui/streams/_filter_form.html.erb +207 -0
- data/app/views/solid_log/ui/streams/_footer.html.erb +37 -0
- data/app/views/solid_log/ui/streams/_log_entries.html.erb +5 -0
- data/app/views/solid_log/ui/streams/_log_row.html.erb +5 -0
- data/app/views/solid_log/ui/streams/_log_row_compact.html.erb +37 -0
- data/app/views/solid_log/ui/streams/_log_row_expanded.html.erb +67 -0
- data/app/views/solid_log/ui/streams/_log_stream_content.html.erb +8 -0
- data/app/views/solid_log/ui/streams/_timeline.html.erb +68 -0
- data/app/views/solid_log/ui/streams/index.html.erb +22 -0
- data/app/views/solid_log/ui/timelines/show_job.html.erb +78 -0
- data/app/views/solid_log/ui/timelines/show_request.html.erb +88 -0
- data/app/views/solid_log/ui/tokens/index.html.erb +95 -0
- data/app/views/solid_log/ui/tokens/new.html.erb +47 -0
- data/config/importmap.rb +15 -0
- data/config/routes.rb +27 -0
- data/lib/solid_log/ui/api_client.rb +117 -0
- data/lib/solid_log/ui/configuration.rb +99 -0
- data/lib/solid_log/ui/data_source.rb +146 -0
- data/lib/solid_log/ui/engine.rb +76 -0
- data/lib/solid_log/ui/version.rb +5 -0
- data/lib/solid_log/ui.rb +27 -0
- data/lib/solid_log-ui.rb +2 -0
- metadata +290 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Jump to Live button functionality
|
|
2
|
+
(function() {
|
|
3
|
+
let isSetup = false;
|
|
4
|
+
let countdownInterval = null;
|
|
5
|
+
|
|
6
|
+
function clearCountdown() {
|
|
7
|
+
if (countdownInterval) {
|
|
8
|
+
clearInterval(countdownInterval);
|
|
9
|
+
countdownInterval = null;
|
|
10
|
+
|
|
11
|
+
const jumpButton = document.getElementById('jump-to-live');
|
|
12
|
+
if (jumpButton && jumpButton.disabled) {
|
|
13
|
+
jumpButton.disabled = false;
|
|
14
|
+
jumpButton.textContent = '↓ Jump to Live';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function handleJumpToLive(event) {
|
|
20
|
+
const button = event.currentTarget;
|
|
21
|
+
|
|
22
|
+
// Clear any existing countdown first
|
|
23
|
+
clearCountdown();
|
|
24
|
+
|
|
25
|
+
// Disable button for 5 seconds
|
|
26
|
+
button.disabled = true;
|
|
27
|
+
const originalText = button.textContent;
|
|
28
|
+
let countdown = 5;
|
|
29
|
+
|
|
30
|
+
countdownInterval = setInterval(() => {
|
|
31
|
+
countdown--;
|
|
32
|
+
button.textContent = `↓ Jump to Live (${countdown}s)`;
|
|
33
|
+
if (countdown <= 0) {
|
|
34
|
+
clearInterval(countdownInterval);
|
|
35
|
+
countdownInterval = null;
|
|
36
|
+
button.disabled = false;
|
|
37
|
+
button.textContent = originalText;
|
|
38
|
+
}
|
|
39
|
+
}, 1000);
|
|
40
|
+
|
|
41
|
+
// Get current URL and filters
|
|
42
|
+
const url = new URL(window.location.href);
|
|
43
|
+
|
|
44
|
+
// Fetch with turbo_stream format
|
|
45
|
+
fetch(url.pathname + url.search, {
|
|
46
|
+
headers: {
|
|
47
|
+
'Accept': 'text/vnd.turbo-stream.html',
|
|
48
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
.then(response => response.text())
|
|
52
|
+
.then(html => {
|
|
53
|
+
if (html && html.trim()) {
|
|
54
|
+
Turbo.renderStreamMessage(html);
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
.catch(error => {
|
|
58
|
+
console.error('Error jumping to live:', error);
|
|
59
|
+
// Re-enable button immediately on error
|
|
60
|
+
clearCountdown();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setupJumpToLive() {
|
|
65
|
+
if (isSetup) return; // Prevent multiple setups
|
|
66
|
+
|
|
67
|
+
const jumpButton = document.getElementById('jump-to-live');
|
|
68
|
+
|
|
69
|
+
if (jumpButton) {
|
|
70
|
+
// Remove any existing listener first
|
|
71
|
+
jumpButton.removeEventListener('click', handleJumpToLive);
|
|
72
|
+
// Add the listener
|
|
73
|
+
jumpButton.addEventListener('click', handleJumpToLive);
|
|
74
|
+
isSetup = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Setup on load
|
|
79
|
+
if (document.readyState === 'loading') {
|
|
80
|
+
document.addEventListener('DOMContentLoaded', setupJumpToLive);
|
|
81
|
+
} else {
|
|
82
|
+
setupJumpToLive();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Re-setup on turbo navigation
|
|
86
|
+
document.addEventListener('turbo:load', () => {
|
|
87
|
+
isSetup = false; // Reset flag for new page
|
|
88
|
+
setupJumpToLive();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Scroll to bottom after Turbo Stream renders (but NOT for live tail updates)
|
|
92
|
+
document.addEventListener('turbo:before-stream-render', function(event) {
|
|
93
|
+
// In Turbo 8, check event.target for the stream element
|
|
94
|
+
const streamElement = event.target;
|
|
95
|
+
|
|
96
|
+
if (streamElement && streamElement.tagName === 'TURBO-STREAM') {
|
|
97
|
+
const target = streamElement.getAttribute('target');
|
|
98
|
+
const action = streamElement.getAttribute('action');
|
|
99
|
+
|
|
100
|
+
// Only scroll for 'replace' actions (Jump to Live), not 'append' (live tail)
|
|
101
|
+
// Live tail uses 'append', Jump to Live uses 'replace'
|
|
102
|
+
if (target === 'log-stream-content' && action === 'replace') {
|
|
103
|
+
// Wait for DOM to fully update, then scroll
|
|
104
|
+
requestAnimationFrame(() => {
|
|
105
|
+
requestAnimationFrame(() => {
|
|
106
|
+
if (window.SolidLogStream && window.SolidLogStream.scrollToBottom) {
|
|
107
|
+
window.SolidLogStream.scrollToBottom();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Export for live tail to clear countdown when new entries arrive
|
|
116
|
+
window.SolidLogJumpToLive = {
|
|
117
|
+
clearCountdown: clearCountdown
|
|
118
|
+
};
|
|
119
|
+
})();
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
// Live Tail functionality for SolidLog streams
|
|
2
|
+
// Supports both WebSocket (ActionCable) and HTTP polling modes
|
|
3
|
+
(function() {
|
|
4
|
+
let liveTailActive = false;
|
|
5
|
+
let pollingInterval = null;
|
|
6
|
+
let cableSubscription = null;
|
|
7
|
+
let lastEntryId = null;
|
|
8
|
+
let mode = null;
|
|
9
|
+
let isInitialFetch = true; // Skip highlighting on first fetch after starting live tail
|
|
10
|
+
|
|
11
|
+
function initializeLiveTail() {
|
|
12
|
+
const toggleButton = document.getElementById('live-tail-toggle');
|
|
13
|
+
if (!toggleButton) return;
|
|
14
|
+
|
|
15
|
+
mode = toggleButton.dataset.liveTailMode;
|
|
16
|
+
if (!mode || mode === 'disabled') return;
|
|
17
|
+
|
|
18
|
+
// Check if already initialized to prevent duplicate listeners
|
|
19
|
+
if (toggleButton.dataset.liveTailInitialized === 'true') {
|
|
20
|
+
// Still need to set up scroll listener even if already initialized
|
|
21
|
+
// because .log-stream element might have been replaced
|
|
22
|
+
setupScrollListener();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
toggleButton.addEventListener('click', function(e) {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
toggleLiveTail();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Mark as initialized
|
|
32
|
+
toggleButton.dataset.liveTailInitialized = 'true';
|
|
33
|
+
|
|
34
|
+
// Set up jump-to-live button
|
|
35
|
+
initializeJumpToLive();
|
|
36
|
+
|
|
37
|
+
// Set up scroll listener
|
|
38
|
+
setupScrollListener();
|
|
39
|
+
|
|
40
|
+
// Store last entry ID for tracking
|
|
41
|
+
updateLastEntryId();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setupScrollListener() {
|
|
45
|
+
const logStream = document.querySelector('.log-stream');
|
|
46
|
+
if (logStream) {
|
|
47
|
+
// Remove existing listener if any to prevent duplicates
|
|
48
|
+
logStream.removeEventListener('scroll', checkScrollPosition);
|
|
49
|
+
logStream.addEventListener('scroll', checkScrollPosition, { passive: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toggleLiveTail() {
|
|
54
|
+
liveTailActive = !liveTailActive;
|
|
55
|
+
const button = document.getElementById('live-tail-toggle');
|
|
56
|
+
const indicator = document.getElementById('live-tail-indicator');
|
|
57
|
+
|
|
58
|
+
if (liveTailActive) {
|
|
59
|
+
startLiveTail();
|
|
60
|
+
button.textContent = '⏸ Pause';
|
|
61
|
+
button.classList.add('btn-primary');
|
|
62
|
+
button.classList.remove('btn-secondary');
|
|
63
|
+
|
|
64
|
+
// Show indicator
|
|
65
|
+
if (indicator) {
|
|
66
|
+
indicator.style.display = 'inline-flex';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Show toast notification
|
|
70
|
+
if (window.SolidLogToast) {
|
|
71
|
+
window.SolidLogToast.show(`Live tail ${mode === 'websocket' ? 'streaming' : 'polling'} started`, 'info');
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
stopLiveTail();
|
|
75
|
+
button.textContent = '▶ Live Tail';
|
|
76
|
+
button.classList.remove('btn-primary');
|
|
77
|
+
button.classList.add('btn-secondary');
|
|
78
|
+
|
|
79
|
+
// Hide indicator
|
|
80
|
+
if (indicator) {
|
|
81
|
+
indicator.style.display = 'none';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (window.SolidLogToast) {
|
|
85
|
+
window.SolidLogToast.show('Live tail stopped', 'info');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function startLiveTail() {
|
|
91
|
+
// Reset flag - next fetch will be the initial one
|
|
92
|
+
isInitialFetch = true;
|
|
93
|
+
|
|
94
|
+
// Clear any time filters from URL (start_time, end_time, before_id, after_id)
|
|
95
|
+
clearTimeFilters();
|
|
96
|
+
|
|
97
|
+
console.log('startLiveTail - mode:', mode, 'createConsumer available:', typeof createConsumer !== 'undefined');
|
|
98
|
+
|
|
99
|
+
if (mode === 'websocket' && typeof createConsumer !== 'undefined') {
|
|
100
|
+
console.log('Starting WebSocket tail');
|
|
101
|
+
startWebSocketTail();
|
|
102
|
+
} else {
|
|
103
|
+
console.log('Starting polling tail (fallback or polling mode)');
|
|
104
|
+
// Fallback to polling if websocket unavailable or mode is 'polling'
|
|
105
|
+
startPollingTail();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Don't auto-scroll - let user maintain their position
|
|
109
|
+
// Show jump-to-live button if new entries arrive while scrolled up
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function stopLiveTail() {
|
|
113
|
+
if (cableSubscription) {
|
|
114
|
+
cableSubscription.unsubscribe();
|
|
115
|
+
cableSubscription = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (pollingInterval) {
|
|
119
|
+
clearInterval(pollingInterval);
|
|
120
|
+
pollingInterval = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function startWebSocketTail() {
|
|
125
|
+
// Get current filter params
|
|
126
|
+
const filters = getCurrentFilters();
|
|
127
|
+
|
|
128
|
+
// Create ActionCable subscription
|
|
129
|
+
const consumer = createConsumer();
|
|
130
|
+
cableSubscription = consumer.subscriptions.create(
|
|
131
|
+
{
|
|
132
|
+
channel: "SolidLog::UI::LogStreamChannel",
|
|
133
|
+
filters: filters
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
connected() {
|
|
137
|
+
console.log('Connected to log stream');
|
|
138
|
+
|
|
139
|
+
// Send heartbeat every 2 minutes to keep cache entry alive
|
|
140
|
+
this.heartbeatInterval = setInterval(() => {
|
|
141
|
+
this.perform('refresh_subscription');
|
|
142
|
+
}, 2 * 60 * 1000);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
disconnected() {
|
|
146
|
+
console.log('Disconnected from log stream');
|
|
147
|
+
|
|
148
|
+
// Clear heartbeat interval
|
|
149
|
+
if (this.heartbeatInterval) {
|
|
150
|
+
clearInterval(this.heartbeatInterval);
|
|
151
|
+
this.heartbeatInterval = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Reset button state to show stream has stopped
|
|
155
|
+
resetLiveTailButton();
|
|
156
|
+
|
|
157
|
+
// If still active, fallback to polling
|
|
158
|
+
if (liveTailActive) {
|
|
159
|
+
console.log('Falling back to polling mode');
|
|
160
|
+
if (window.SolidLogToast) {
|
|
161
|
+
window.SolidLogToast.show('Connection lost, switching to polling mode', 'warning');
|
|
162
|
+
}
|
|
163
|
+
startPollingTail();
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
received(data) {
|
|
168
|
+
// Received new log entry via websocket (already filtered server-side)
|
|
169
|
+
console.log('[LiveTail] WebSocket received data:', data, 'isInitialFetch:', isInitialFetch, 'has html:', !!data.html);
|
|
170
|
+
if (data.html) {
|
|
171
|
+
console.log('[LiveTail] Appending entry with ID:', data.entry_id);
|
|
172
|
+
appendEntry(data.html);
|
|
173
|
+
updateLastEntryId();
|
|
174
|
+
|
|
175
|
+
// Only highlight if this is NOT the initial message
|
|
176
|
+
if (!isInitialFetch) {
|
|
177
|
+
console.log('[LiveTail] Not initial message - highlighting button');
|
|
178
|
+
highlightJumpToLive();
|
|
179
|
+
// Update timeline to show new data has arrived
|
|
180
|
+
updateTimeline();
|
|
181
|
+
} else {
|
|
182
|
+
console.log('[LiveTail] Initial message - skipping highlight');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Mark that we've received the initial message
|
|
186
|
+
isInitialFetch = false;
|
|
187
|
+
} else {
|
|
188
|
+
console.log('[LiveTail] No HTML in data, ignoring');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function startPollingTail() {
|
|
196
|
+
// Poll every 2 seconds
|
|
197
|
+
pollingInterval = setInterval(function() {
|
|
198
|
+
fetchNewEntries();
|
|
199
|
+
}, 2000);
|
|
200
|
+
|
|
201
|
+
// Initial fetch
|
|
202
|
+
fetchNewEntries();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function fetchNewEntries() {
|
|
206
|
+
const streamsPath = document.body.dataset.streamsPath || '/streams';
|
|
207
|
+
const url = new URL(window.location.origin + streamsPath);
|
|
208
|
+
|
|
209
|
+
// Copy current filters
|
|
210
|
+
const currentParams = new URLSearchParams(window.location.search);
|
|
211
|
+
currentParams.forEach((value, key) => {
|
|
212
|
+
url.searchParams.append(key, value);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Add after_id parameter if we have a last entry
|
|
216
|
+
if (lastEntryId) {
|
|
217
|
+
url.searchParams.set('after_id', lastEntryId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Request turbo stream format
|
|
221
|
+
fetch(url.toString(), {
|
|
222
|
+
headers: {
|
|
223
|
+
'Accept': 'text/vnd.turbo-stream.html',
|
|
224
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
.then(response => {
|
|
228
|
+
if (!response.ok) throw new Error('Network response was not ok');
|
|
229
|
+
return response.text();
|
|
230
|
+
})
|
|
231
|
+
.then(html => {
|
|
232
|
+
console.log('Polling received HTML, length:', html ? html.length : 0, 'isInitialFetch:', isInitialFetch);
|
|
233
|
+
if (html && html.trim()) {
|
|
234
|
+
// Turbo will automatically process the stream response
|
|
235
|
+
Turbo.renderStreamMessage(html);
|
|
236
|
+
updateLastEntryId();
|
|
237
|
+
|
|
238
|
+
// Only highlight if this is NOT the initial fetch
|
|
239
|
+
if (!isInitialFetch) {
|
|
240
|
+
console.log('Not initial fetch - highlighting button');
|
|
241
|
+
highlightJumpToLive();
|
|
242
|
+
// Update timeline to show new data has arrived
|
|
243
|
+
updateTimeline();
|
|
244
|
+
} else {
|
|
245
|
+
console.log('Initial fetch - skipping highlight');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Mark that we've completed the initial fetch
|
|
249
|
+
isInitialFetch = false;
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
.catch(error => {
|
|
253
|
+
console.error('Error fetching new logs:', error);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function appendEntry(html) {
|
|
258
|
+
const logStream = document.getElementById('log-stream-content');
|
|
259
|
+
if (!logStream) return;
|
|
260
|
+
|
|
261
|
+
const temp = document.createElement('div');
|
|
262
|
+
temp.innerHTML = html;
|
|
263
|
+
|
|
264
|
+
// Append new entries
|
|
265
|
+
while (temp.firstChild) {
|
|
266
|
+
logStream.appendChild(temp.firstChild);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function updateLastEntryId() {
|
|
271
|
+
const logStream = document.getElementById('log-stream-content');
|
|
272
|
+
if (!logStream) return;
|
|
273
|
+
|
|
274
|
+
const entries = logStream.querySelectorAll('[data-entry-id]');
|
|
275
|
+
if (entries.length > 0) {
|
|
276
|
+
const lastEntry = entries[entries.length - 1];
|
|
277
|
+
lastEntryId = lastEntry.dataset.entryId;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function clearTimeFilters() {
|
|
282
|
+
// Remove time-based filters from URL when starting live tail
|
|
283
|
+
const url = new URL(window.location.href);
|
|
284
|
+
const params = url.searchParams;
|
|
285
|
+
|
|
286
|
+
// Remove time filters
|
|
287
|
+
params.delete('filters[start_time]');
|
|
288
|
+
params.delete('filters[end_time]');
|
|
289
|
+
params.delete('before_id');
|
|
290
|
+
params.delete('after_id');
|
|
291
|
+
|
|
292
|
+
// Update URL without reloading the page
|
|
293
|
+
window.history.replaceState({}, '', url);
|
|
294
|
+
|
|
295
|
+
// Clear the timeline selection visually if it exists
|
|
296
|
+
const timelineController = document.querySelector('[data-controller="timeline-histogram"]');
|
|
297
|
+
if (timelineController && window.Stimulus) {
|
|
298
|
+
const controller = window.Stimulus.getControllerForElementAndIdentifier(timelineController, 'timeline-histogram');
|
|
299
|
+
if (controller && controller.clearSelection) {
|
|
300
|
+
controller.clearSelection();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function updateTimeline() {
|
|
306
|
+
// Fetch updated timeline data when new entries arrive during live tail
|
|
307
|
+
const url = new URL(window.location.href);
|
|
308
|
+
url.searchParams.set('timeline_only', '1');
|
|
309
|
+
|
|
310
|
+
fetch(url, {
|
|
311
|
+
headers: {
|
|
312
|
+
'Accept': 'text/vnd.turbo-stream.html',
|
|
313
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
.then(response => response.text())
|
|
317
|
+
.then(html => {
|
|
318
|
+
if (html && html.trim()) {
|
|
319
|
+
Turbo.renderStreamMessage(html);
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
.catch(error => {
|
|
323
|
+
console.error('Error updating timeline:', error);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function getCurrentFilters() {
|
|
328
|
+
const filters = {};
|
|
329
|
+
const params = new URLSearchParams(window.location.search);
|
|
330
|
+
|
|
331
|
+
params.forEach((value, key) => {
|
|
332
|
+
if (key.startsWith('filters[')) {
|
|
333
|
+
const filterKey = key.match(/filters\[(.*?)\]/)[1];
|
|
334
|
+
// Skip time filters - they should be cleared for live tail
|
|
335
|
+
if (filterKey === 'start_time' || filterKey === 'end_time') {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!filters[filterKey]) {
|
|
339
|
+
filters[filterKey] = [];
|
|
340
|
+
}
|
|
341
|
+
filters[filterKey].push(value);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return filters;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function resetLiveTailButton() {
|
|
349
|
+
const button = document.getElementById('live-tail-toggle');
|
|
350
|
+
const indicator = document.getElementById('live-tail-indicator');
|
|
351
|
+
|
|
352
|
+
if (button) {
|
|
353
|
+
button.textContent = '▶ Live Tail';
|
|
354
|
+
button.classList.remove('btn-primary');
|
|
355
|
+
button.classList.add('btn-secondary');
|
|
356
|
+
liveTailActive = false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (indicator) {
|
|
360
|
+
indicator.style.display = 'none';
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function scrollToBottom() {
|
|
365
|
+
setTimeout(() => {
|
|
366
|
+
if (window.SolidLogStream && window.SolidLogStream.scrollToBottom) {
|
|
367
|
+
window.SolidLogStream.scrollToBottom();
|
|
368
|
+
} else {
|
|
369
|
+
const logStream = document.querySelector('.log-stream');
|
|
370
|
+
if (logStream) {
|
|
371
|
+
logStream.scrollTop = logStream.scrollHeight;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}, 50);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function initializeJumpToLive() {
|
|
378
|
+
const jumpButton = document.getElementById('jump-to-live');
|
|
379
|
+
if (!jumpButton) return;
|
|
380
|
+
|
|
381
|
+
// Check if already initialized
|
|
382
|
+
if (jumpButton.dataset.jumpInitialized === 'true') {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
jumpButton.addEventListener('click', function(e) {
|
|
387
|
+
e.preventDefault();
|
|
388
|
+
scrollToBottom();
|
|
389
|
+
clearJumpToLiveHighlight();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
jumpButton.dataset.jumpInitialized = 'true';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function highlightJumpToLive() {
|
|
396
|
+
const jumpButton = document.getElementById('jump-to-live');
|
|
397
|
+
if (!jumpButton) return;
|
|
398
|
+
|
|
399
|
+
console.log('highlightJumpToLive() called');
|
|
400
|
+
|
|
401
|
+
// Clear countdown timer if button is disabled
|
|
402
|
+
if (window.SolidLogJumpToLive && window.SolidLogJumpToLive.clearCountdown) {
|
|
403
|
+
window.SolidLogJumpToLive.clearCountdown();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Always highlight when new entries arrive
|
|
407
|
+
console.log('Adding has-new-entries class to button');
|
|
408
|
+
jumpButton.classList.add('has-new-entries');
|
|
409
|
+
console.log('Button classes now:', jumpButton.className);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function clearJumpToLiveHighlight() {
|
|
413
|
+
const jumpButton = document.getElementById('jump-to-live');
|
|
414
|
+
if (jumpButton) {
|
|
415
|
+
jumpButton.classList.remove('has-new-entries');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function checkScrollPosition() {
|
|
420
|
+
// Clear highlight when user scrolls to bottom
|
|
421
|
+
const atBottom = isAtBottom();
|
|
422
|
+
console.log('[LiveTail] checkScrollPosition - atBottom:', atBottom);
|
|
423
|
+
if (atBottom) {
|
|
424
|
+
console.log('[LiveTail] Clearing jump-to-live highlight (user scrolled to bottom)');
|
|
425
|
+
clearJumpToLiveHighlight();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function isAtBottom() {
|
|
430
|
+
const logStream = document.querySelector('.log-stream');
|
|
431
|
+
if (!logStream) return true;
|
|
432
|
+
|
|
433
|
+
// Consider "at bottom" if within 10px of the bottom
|
|
434
|
+
const scrollDistance = logStream.scrollHeight - logStream.scrollTop - logStream.clientHeight;
|
|
435
|
+
console.log('[LiveTail] isAtBottom check - scrollHeight:', logStream.scrollHeight,
|
|
436
|
+
'scrollTop:', logStream.scrollTop, 'clientHeight:', logStream.clientHeight,
|
|
437
|
+
'scrollDistance:', scrollDistance);
|
|
438
|
+
return scrollDistance < 10;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Initialize on page load
|
|
442
|
+
if (document.readyState === 'loading') {
|
|
443
|
+
document.addEventListener('DOMContentLoaded', initializeLiveTail);
|
|
444
|
+
} else {
|
|
445
|
+
initializeLiveTail();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Re-initialize on turbo navigation
|
|
449
|
+
document.addEventListener('turbo:load', function() {
|
|
450
|
+
// Reset state on new page
|
|
451
|
+
liveTailActive = false;
|
|
452
|
+
lastEntryId = null;
|
|
453
|
+
|
|
454
|
+
initializeLiveTail();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Stop live tail when navigating away or changing filters
|
|
458
|
+
document.addEventListener('turbo:before-visit', function() {
|
|
459
|
+
if (liveTailActive) {
|
|
460
|
+
stopLiveTail();
|
|
461
|
+
liveTailActive = false;
|
|
462
|
+
console.log('Stopped live tail due to navigation');
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Stop live tail when filter form is submitted
|
|
467
|
+
document.addEventListener('submit', function(e) {
|
|
468
|
+
if (e.target.closest('.filter-form') && liveTailActive) {
|
|
469
|
+
stopLiveTail();
|
|
470
|
+
console.log('Stopped live tail due to filter change');
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Clean up on page unload
|
|
475
|
+
window.addEventListener('beforeunload', stopLiveTail);
|
|
476
|
+
})();
|