onlylogs 0.1.2

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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +311 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/config/onlylogs_manifest.js +2 -0
  5. data/app/assets/images/onlylogs/favicon/apple-touch-icon.png +0 -0
  6. data/app/assets/images/onlylogs/favicon/favicon-96x96.png +0 -0
  7. data/app/assets/images/onlylogs/favicon/favicon.ico +0 -0
  8. data/app/assets/images/onlylogs/favicon/favicon.svg +3 -0
  9. data/app/assets/images/onlylogs/favicon/site.webmanifest.erb +21 -0
  10. data/app/assets/images/onlylogs/favicon/web-app-manifest-192x192.png +0 -0
  11. data/app/assets/images/onlylogs/favicon/web-app-manifest-512x512.png +0 -0
  12. data/app/assets/images/onlylogs/logo.png +0 -0
  13. data/app/channels/onlylogs/application_cable/channel.rb +11 -0
  14. data/app/channels/onlylogs/logs_channel.rb +181 -0
  15. data/app/controllers/onlylogs/application_controller.rb +22 -0
  16. data/app/controllers/onlylogs/logs_controller.rb +23 -0
  17. data/app/helpers/onlylogs/application_helper.rb +4 -0
  18. data/app/javascript/onlylogs/application.js +1 -0
  19. data/app/javascript/onlylogs/controllers/application.js +9 -0
  20. data/app/javascript/onlylogs/controllers/index.js +11 -0
  21. data/app/javascript/onlylogs/controllers/keyboard_shortcuts_controller.js +46 -0
  22. data/app/javascript/onlylogs/controllers/log_streamer_controller.js +432 -0
  23. data/app/javascript/onlylogs/controllers/text_selection_controller.js +90 -0
  24. data/app/jobs/onlylogs/application_job.rb +4 -0
  25. data/app/models/onlylogs/ansi_color_parser.rb +78 -0
  26. data/app/models/onlylogs/application_record.rb +5 -0
  27. data/app/models/onlylogs/batch_sender.rb +61 -0
  28. data/app/models/onlylogs/file.rb +151 -0
  29. data/app/models/onlylogs/file_path_parser.rb +118 -0
  30. data/app/models/onlylogs/grep.rb +54 -0
  31. data/app/models/onlylogs/log_line.rb +24 -0
  32. data/app/models/onlylogs/secure_file_path.rb +31 -0
  33. data/app/views/home/show.html.erb +10 -0
  34. data/app/views/layouts/onlylogs/application.html.erb +27 -0
  35. data/app/views/onlylogs/logs/index.html.erb +49 -0
  36. data/app/views/onlylogs/shared/_log_container.html.erb +106 -0
  37. data/app/views/onlylogs/shared/_log_container_styles.html.erb +228 -0
  38. data/config/importmap.rb +6 -0
  39. data/config/puma_plugins/vector.rb +94 -0
  40. data/config/routes.rb +4 -0
  41. data/config/udp_logger.rb +40 -0
  42. data/config/vector.toml +32 -0
  43. data/db/migrate/20250902112548_create_books.rb +9 -0
  44. data/lib/onlylogs/configuration.rb +133 -0
  45. data/lib/onlylogs/engine.rb +39 -0
  46. data/lib/onlylogs/formatter.rb +14 -0
  47. data/lib/onlylogs/log_silencer_middleware.rb +26 -0
  48. data/lib/onlylogs/logger.rb +10 -0
  49. data/lib/onlylogs/socket_logger.rb +71 -0
  50. data/lib/onlylogs/version.rb +3 -0
  51. data/lib/onlylogs.rb +17 -0
  52. data/lib/puma/plugin/onlylogs_sidecar.rb +113 -0
  53. data/lib/tasks/onlylogs_tasks.rake +4 -0
  54. metadata +110 -0
@@ -0,0 +1,22 @@
1
+ module Onlylogs
2
+ class ApplicationController < (Onlylogs.parent_controller&.constantize || ActionController::Base)
3
+ before_action :authenticate_onlylogs_user!
4
+
5
+ private
6
+
7
+ def authenticate_onlylogs_user!
8
+ return super if defined?(super)
9
+
10
+ return if Onlylogs.disable_basic_authentication?
11
+
12
+ unless Onlylogs.basic_auth_configured?
13
+ render plain: "Onlylogs authentication not configured. Please configure basic auth credentials.", status: :forbidden
14
+ return
15
+ end
16
+
17
+ authenticate_or_request_with_http_basic("onlylogs") do |username, password|
18
+ username == Onlylogs.basic_auth_user && password == Onlylogs.basic_auth_password
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Onlylogs
4
+ class LogsController < ApplicationController
5
+ def index
6
+ @max_lines = (params[:max_lines] || 100).to_i
7
+
8
+ # Get the file path from params or use default
9
+ @log_file_path = params[:log_file_path] || default_log_file_path
10
+
11
+ @filter = params[:filter]
12
+ @autoscroll = params[:autoscroll] != "false"
13
+ @mode = @filter.blank? ? (params[:mode] || "live") : "search" # "live" or "search"
14
+ end
15
+
16
+ private
17
+
18
+ def default_log_file_path
19
+ # "/Users/alessandrorodi/RenuoWorkspace/onlylogs/test/fixtures/files/very_big.log"
20
+ Onlylogs.default_log_file_path
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,4 @@
1
+ module Onlylogs
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1 @@
1
+ import "controllers"
@@ -0,0 +1,9 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+
3
+ const application = Application.start()
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false
7
+ window.Stimulus = application
8
+
9
+ export { application }
@@ -0,0 +1,11 @@
1
+ // Import and register all your controllers from the importmap under controllers/*
2
+
3
+ import { application } from "controllers/application"
4
+
5
+ // Eager load all controllers defined in the import map under controllers/**/*_controller
6
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
7
+ eagerLoadControllersFrom("controllers", application)
8
+
9
+ // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
10
+ // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
11
+ // lazyLoadControllersFrom("controllers", application)
@@ -0,0 +1,46 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class KeyboardShortcutsController extends Controller {
4
+ static targets = ["liveMode", "autoscroll"]
5
+
6
+ connect() {
7
+ this.boundHandleKeydown = this.handleKeydown.bind(this)
8
+ document.addEventListener('keydown', this.boundHandleKeydown)
9
+ }
10
+
11
+ disconnect() {
12
+ document.removeEventListener('keydown', this.boundHandleKeydown)
13
+ }
14
+
15
+ handleKeydown(event) {
16
+ // Only handle shortcuts when not typing in input fields
17
+ if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
18
+ return
19
+ }
20
+
21
+ switch (event.key.toLowerCase()) {
22
+ case 'l':
23
+ event.preventDefault()
24
+ this.toggleLiveMode()
25
+ break
26
+ case 'a':
27
+ event.preventDefault()
28
+ this.toggleAutoscroll()
29
+ break
30
+ }
31
+ }
32
+
33
+ toggleLiveMode() {
34
+ if (this.hasLiveModeTarget) {
35
+ this.liveModeTarget.checked = !this.liveModeTarget.checked
36
+ this.liveModeTarget.dispatchEvent(new Event('change', { bubbles: true }))
37
+ }
38
+ }
39
+
40
+ toggleAutoscroll() {
41
+ if (this.hasAutoscrollTarget) {
42
+ this.autoscrollTarget.checked = !this.autoscrollTarget.checked
43
+ this.autoscrollTarget.dispatchEvent(new Event('change', { bubbles: true }))
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,432 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import { createConsumer } from "@rails/actioncable";
3
+
4
+ export default class LogStreamerController extends Controller {
5
+ static values = {
6
+ filePath: { type: String },
7
+ cursorPosition: { type: Number, default: 0 },
8
+ lastLineNumber: { type: Number, default: 0 },
9
+ autoScroll: { type: Boolean, default: true },
10
+ autoStart: { type: Boolean, default: true },
11
+ filter: { type: String, default: '' },
12
+ mode: { type: String, default: 'live' },
13
+ regexpMode: { type: Boolean, default: false }
14
+ };
15
+
16
+ static targets = ["logLines", "filterInput", "lineRange", "liveMode", "message", "regexpMode", "websocketStatus", "stopButton", "clearButton"];
17
+
18
+ connect() {
19
+ this.consumer = createConsumer();
20
+
21
+ this.subscription = null;
22
+ this.isRunning = false;
23
+ this.reconnectTimeout = null;
24
+ this.minLineNumber = null;
25
+ this.maxLineNumber = 0;
26
+ this.isSearchFinished = true;
27
+
28
+ // Initialize clusterize
29
+ this.clusterize = null;
30
+ this.#initializeClusterize();
31
+
32
+ this.#updateWebsocketStatus('disconnected');
33
+
34
+ this.start();
35
+ this.updateLiveModeState();
36
+ this.scroll();
37
+ }
38
+
39
+ disconnect() {
40
+ this.stop();
41
+
42
+ // Clear any pending reconnect timeout
43
+ if (this.reconnectTimeout) {
44
+ clearTimeout(this.reconnectTimeout);
45
+ this.reconnectTimeout = null;
46
+ }
47
+
48
+ // Destroy clusterize instance
49
+ if (this.clusterize) {
50
+ this.clusterize.destroy();
51
+ this.clusterize = null;
52
+ }
53
+ }
54
+
55
+ start() {
56
+ if (this.isRunning) {
57
+ return;
58
+ }
59
+
60
+ this.isRunning = true;
61
+ this.#createSubscription();
62
+ }
63
+
64
+ stop() {
65
+ if (!this.isRunning) {
66
+ return;
67
+ }
68
+
69
+ this.isRunning = false;
70
+
71
+ if (this.subscription) {
72
+ this.stopSearch();
73
+ this.subscription.unsubscribe();
74
+ this.subscription = null;
75
+ }
76
+ }
77
+
78
+ reset() {
79
+ this.stop();
80
+ this.clear();
81
+ this.#reinitializeClusterize();
82
+ this.start();
83
+ }
84
+
85
+ clear() {
86
+ this.minLineNumber = null;
87
+ this.maxLineNumber = 0;
88
+
89
+ this.clusterize.clear();
90
+
91
+ this.#updateLineRangeDisplay();
92
+ }
93
+
94
+ toggleAutoScroll() {
95
+ this.autoScrollValue = !this.autoScrollValue;
96
+ this.scroll();
97
+ }
98
+
99
+ toggleRegexpMode() {
100
+ this.regexpModeValue = this.regexpModeTarget.checked;
101
+ // If we have a filter applied, reconnect to apply the new regexp mode
102
+ if (this.filterInputTarget.value && this.filterInputTarget.value.trim() !== '') {
103
+ this.reconnectWithNewMode();
104
+ }
105
+ }
106
+
107
+ toggleLiveMode() {
108
+ // this condition looks revered, but the value here has been changed already. so the live mode has been enabled.
109
+ if (this.isLiveMode()) {
110
+ this.clearFilter();
111
+ this.modeValue = 'live';
112
+ this.reconnectWithNewMode();
113
+ return;
114
+ }
115
+ else {
116
+ this.liveModeTarget.checked = true;
117
+ return false;
118
+ }
119
+ }
120
+
121
+ applyFilter() {
122
+ // If filter is applied, disable live mode
123
+ if (this.filterInputTarget.value && this.filterInputTarget.value.trim() !== '') {
124
+ this.liveModeTarget.checked = false;
125
+ this.modeValue = 'search';
126
+ } else {
127
+ // If no filter, enable live mode
128
+ this.liveModeTarget.checked = true;
129
+ this.modeValue = 'live';
130
+ }
131
+
132
+ // Update visual state
133
+ this.updateLiveModeState();
134
+ this.updateStopButtonVisibility();
135
+
136
+ // Use the global debounced reconnection (300ms delay)
137
+ this.reconnectWithNewMode();
138
+ }
139
+
140
+ isLiveMode() {
141
+ return this.liveModeTarget.checked;
142
+ }
143
+
144
+ scroll() {
145
+ if (this.autoScrollValue) {
146
+ this.logLinesTarget.scrollTop = this.logLinesTarget.scrollHeight;
147
+ }
148
+ }
149
+
150
+ reconnectWithNewMode() {
151
+ // Clear any existing reconnect timeout
152
+ if (this.reconnectTimeout) {
153
+ clearTimeout(this.reconnectTimeout);
154
+ }
155
+
156
+ // Debounce reconnection to avoid multiple rapid reconnections
157
+ this.reconnectTimeout = setTimeout(() => {
158
+ this.stop();
159
+ this.clear();
160
+ this.#reinitializeClusterize();
161
+ this.start();
162
+ this.reconnectTimeout = null;
163
+ }, 600);
164
+ }
165
+
166
+ clearFilter() {
167
+ // Clear the filter input
168
+ this.filterInputTarget.value = '';
169
+
170
+ // Re-enable live mode
171
+ this.liveModeTarget.checked = true;
172
+ this.modeValue = 'live';
173
+
174
+ // Update visual state
175
+ this.updateLiveModeState();
176
+ this.updateStopButtonVisibility();
177
+
178
+ // Reconnect with cleared filter and live mode
179
+ this.reconnectWithNewMode();
180
+ }
181
+
182
+ stopSearch() {
183
+ if (this.subscription && this.isRunning) {
184
+ this.subscription.perform('stop_watcher');
185
+ }
186
+ }
187
+
188
+ clearLogs() {
189
+ this.clear();
190
+ this.#hideMessage();
191
+ }
192
+
193
+ updateLiveModeState() {
194
+ const liveModeLabel = this.liveModeTarget.closest('label');
195
+
196
+ if (this.isLiveMode()) {
197
+ liveModeLabel.classList.remove('live-mode-sticky');
198
+ } else {
199
+ liveModeLabel.classList.add('live-mode-sticky');
200
+ }
201
+ }
202
+
203
+ updateStopButtonVisibility() {
204
+ const shouldShow = !this.isLiveMode() && this.subscription && this.isRunning && !this.isSearchFinished;
205
+ this.stopButtonTarget.style.display = shouldShow ? 'inline-block' : 'none';
206
+ }
207
+
208
+
209
+ /**
210
+ * Create ActionCable subscription
211
+ */
212
+ #createSubscription() {
213
+ this.subscription = this.consumer.subscriptions.create("Onlylogs::LogsChannel", {
214
+ connected: () => {
215
+ this.#handleConnected();
216
+ },
217
+
218
+ disconnected: () => {
219
+ this.#handleDisconnected();
220
+ },
221
+
222
+ rejected: () => {
223
+ this.#handleRejected();
224
+ },
225
+
226
+ received: (data) => {
227
+ if (data.action === 'append_logs') {
228
+ this.isSearchFinished = this.isLiveMode();
229
+ this.#handleLogLines(data.lines);
230
+ } else if (data.action === 'message') {
231
+ this.#handleMessage(data.content);
232
+ } else if (data.action === 'finish') {
233
+ this.#handleFinish(data.content);
234
+ } else if (data.action === 'error') {
235
+ this.#handleError(data.content);
236
+ }
237
+ }
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Handle successful connection
243
+ */
244
+ #handleConnected() {
245
+ this.subscription.perform('initialize_watcher', {
246
+ cursor_position: this.cursorPositionValue,
247
+ last_line_number: this.lastLineNumberValue,
248
+ file_path: this.filePathValue,
249
+ filter: this.filterInputTarget.value,
250
+ mode: this.modeValue,
251
+ regexp_mode: this.regexpModeValue
252
+ });
253
+
254
+ this.element.classList.add("log-streamer--connected");
255
+ this.element.classList.remove("log-streamer--disconnected", "log-streamer--rejected");
256
+ this.#updateWebsocketStatus('connected');
257
+ this.updateStopButtonVisibility();
258
+ }
259
+
260
+ #handleDisconnected() {
261
+ this.element.classList.add("log-streamer--disconnected");
262
+ this.element.classList.remove("log-streamer--connected");
263
+ this.#updateWebsocketStatus('disconnected');
264
+ this.updateStopButtonVisibility();
265
+ }
266
+
267
+ #handleRejected() {
268
+ this.element.classList.add("log-streamer--rejected");
269
+ this.element.classList.remove("log-streamer--connected", "log-streamer--disconnected");
270
+ this.#updateWebsocketStatus('rejected');
271
+ this.updateStopButtonVisibility();
272
+ }
273
+
274
+ #handleLogLines(lines) {
275
+ try {
276
+ const newLines = [];
277
+
278
+ lines.forEach(line => {
279
+ const { line_number, html } = line;
280
+
281
+ if (this.minLineNumber === null || line_number < this.minLineNumber) {
282
+ this.minLineNumber = line_number;
283
+ }
284
+ this.maxLineNumber = Math.max(this.maxLineNumber, line_number);
285
+
286
+ // Add to new lines array for clusterize
287
+ newLines.push(html);
288
+ });
289
+
290
+ // Append new lines to clusterize
291
+ if (newLines.length > 0) {
292
+ this.clusterize.append(newLines);
293
+ this.#updateLineRangeDisplay();
294
+ this.scroll();
295
+ }
296
+
297
+ // Update stop button visibility after processing lines
298
+ this.updateStopButtonVisibility();
299
+
300
+ } catch (error) {
301
+ console.error('Error handling log lines:', error);
302
+ }
303
+ }
304
+
305
+ #handleMessage(message) {
306
+ this.#hideMessage();
307
+ if (message === '') {
308
+ this.messageTarget.innerHTML = "";
309
+ } else {
310
+ const loadingIcon = message.endsWith('...') ? '<span class="onlylogs-spin-animation">⟳</span>' : '';
311
+ this.messageTarget.innerHTML = loadingIcon + message;
312
+ }
313
+ }
314
+
315
+ #handleFinish(message) {
316
+ // Display the finish message without loading icon
317
+ this.messageTarget.innerHTML = message;
318
+
319
+ // Mark search as finished
320
+ this.isSearchFinished = true;
321
+
322
+ // Update stop button visibility (should hide it)
323
+ this.updateStopButtonVisibility();
324
+ }
325
+
326
+ #handleError(message) {
327
+ // Display error message with error styling
328
+ this.messageTarget.innerHTML = `<span class="error-message">❌ ${message}</span>`;
329
+
330
+ // Mark search as finished
331
+ this.isSearchFinished = true;
332
+
333
+ // Update stop button visibility (should hide it)
334
+ this.updateStopButtonVisibility();
335
+
336
+ // Stop the watcher
337
+ this.stop();
338
+ }
339
+
340
+ #hideMessage() {
341
+ this.messageTarget.innerHTML = '';
342
+ }
343
+
344
+ #updateLineRangeDisplay() {
345
+ const resultsCount = this.clusterize.getRowsAmount();
346
+ let lineRangeText;
347
+
348
+ if (this.minLineNumber === null || this.maxLineNumber === 0) {
349
+ lineRangeText = `No lines`;
350
+ } else if (this.minLineNumber === this.maxLineNumber) {
351
+ lineRangeText = `Line ${this.#formatNumber(this.minLineNumber)}`;
352
+ } else {
353
+ lineRangeText = `Lines ${this.#formatNumber(this.minLineNumber)} - ${this.#formatNumber(this.maxLineNumber)}`;
354
+ }
355
+
356
+ this.lineRangeTarget.textContent = `${lineRangeText} | Results: ${this.#formatNumber(resultsCount)}`;
357
+ }
358
+
359
+ #formatNumber(number) {
360
+ return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "'");
361
+ }
362
+
363
+ #updateWebsocketStatus(status) {
364
+ if (!this.hasWebsocketStatusTarget) {
365
+ return;
366
+ }
367
+
368
+ const statusElement = this.websocketStatusTarget;
369
+ statusElement.className = `websocket-status websocket-status--${status}`;
370
+
371
+ switch (status) {
372
+ case 'connected':
373
+ statusElement.innerHTML = '🟢';
374
+ statusElement.title = 'WebSocket Connected';
375
+ break;
376
+ case 'disconnected':
377
+ statusElement.innerHTML = '🔴';
378
+ statusElement.title = 'WebSocket Disconnected';
379
+ break;
380
+ case 'rejected':
381
+ statusElement.innerHTML = '🟡';
382
+ statusElement.title = 'WebSocket Connection Rejected';
383
+ break;
384
+ default:
385
+ statusElement.innerHTML = '⚪';
386
+ statusElement.title = 'WebSocket Status Unknown';
387
+ }
388
+ }
389
+
390
+ getStatus() {
391
+ return {
392
+ isRunning: this.isRunning,
393
+ filePath: this.filePathValue,
394
+ cursorPosition: this.cursorPositionValue,
395
+ lineCount: this.clusterize.getRowsAmount(),
396
+ maxLineNumber: this.maxLineNumber,
397
+ connected: this.subscription && this.subscription.identifier
398
+ };
399
+ }
400
+
401
+ #initializeClusterize() {
402
+ this.clusterize = new window.Clusterize({
403
+ scrollId: 'scrollArea',
404
+ contentId: 'contentArea',
405
+ rows: [],
406
+ tag: 'pre',
407
+ rows_in_block: 50,
408
+ blocks_in_cluster: 4,
409
+ show_no_data_row: false,
410
+ no_data_text: 'No log lines available',
411
+ no_data_class: 'clusterize-no-data',
412
+ keep_parity: true,
413
+ callbacks: {
414
+ clusterWillChange: () => {
415
+ // Optional: handle cluster change
416
+ },
417
+ clusterChanged: () => {
418
+ // Optional: handle after cluster change
419
+ },
420
+ scrollingProgress: (progress) => {
421
+ // Optional: handle scrolling progress
422
+ }
423
+ }
424
+ });
425
+ }
426
+
427
+ #reinitializeClusterize() {
428
+ this.clusterize.destroy();
429
+ this.clusterize = null;
430
+ this.#initializeClusterize();
431
+ }
432
+ }
@@ -0,0 +1,90 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class TextSelectionController extends Controller {
4
+ static targets = ["button", "filterInput", "logLines", "regexpMode"]
5
+
6
+ connect() {
7
+ this.boundHandleTextSelection = this.handleTextSelection.bind(this)
8
+ this.boundHandleDocumentClick = this.handleDocumentClick.bind(this)
9
+ this.boundHandleSelectionChange = this.handleSelectionChange.bind(this)
10
+
11
+ // Only listen for text selection on the log lines, not the toolbar
12
+ this.logLinesTarget.addEventListener('mouseup', this.boundHandleTextSelection)
13
+ document.addEventListener('click', this.boundHandleDocumentClick)
14
+ document.addEventListener('selectionchange', this.boundHandleSelectionChange)
15
+ }
16
+
17
+ disconnect() {
18
+ this.logLinesTarget.removeEventListener('mouseup', this.boundHandleTextSelection)
19
+ document.removeEventListener('click', this.boundHandleDocumentClick)
20
+ document.removeEventListener('selectionchange', this.boundHandleSelectionChange)
21
+ }
22
+
23
+ handleTextSelection(event) {
24
+ const selection = window.getSelection()
25
+ const selectedText = selection.toString().trim()
26
+
27
+ // Check if there's actually a text selection
28
+ if (selectedText.length > 0 && selection.rangeCount > 0) {
29
+ this.showButton(event, selectedText)
30
+ } else {
31
+ this.hideButton()
32
+ }
33
+ }
34
+
35
+ showButton(event, selectedText) {
36
+ if (!this.buttonTarget) {
37
+ return
38
+ }
39
+
40
+ const containerRect = this.element.getBoundingClientRect()
41
+
42
+ // Position button below mouse cursor
43
+ const left = event.clientX - containerRect.left - 40 // Roughly center the button
44
+ const top = event.clientY - containerRect.top + 10
45
+
46
+ this.buttonTarget.style.left = Math.max(0, left) + 'px'
47
+ this.buttonTarget.style.top = Math.max(0, top) + 'px'
48
+ this.buttonTarget.style.display = 'block'
49
+
50
+ // Store selected text for search
51
+ this.selectedText = selectedText
52
+ }
53
+
54
+ hideButton() {
55
+ this.buttonTarget.style.display = 'none'
56
+ this.selectedText = null
57
+ }
58
+
59
+ handleDocumentClick(event) {
60
+ // Only hide if clicking outside the button and outside the log lines container
61
+ if (!this.buttonTarget.contains(event.target) && !this.logLinesTarget.contains(event.target)) {
62
+ this.hideButton()
63
+ }
64
+ }
65
+
66
+ handleSelectionChange() {
67
+ // Check if the current selection is still valid
68
+ const selection = window.getSelection()
69
+ const selectedText = selection.toString().trim()
70
+
71
+ // If no text is selected, hide the button
72
+ if (selectedText.length === 0 || selection.rangeCount === 0) {
73
+ this.hideButton()
74
+ }
75
+ }
76
+
77
+ searchSelectedText() {
78
+ if (this.selectedText) {
79
+ if (this.regexpModeTarget.checked) {
80
+ this.regexpModeTarget.checked = false
81
+ this.regexpModeTarget.dispatchEvent(new Event('change', { bubbles: true }))
82
+ }
83
+
84
+ this.filterInputTarget.value = this.selectedText
85
+ this.filterInputTarget.dispatchEvent(new Event('input', { bubbles: true }))
86
+ this.hideButton()
87
+ window.getSelection().removeAllRanges()
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,4 @@
1
+ module Onlylogs
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end