onlylogs 0.11.0 → 0.12.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 +251 -77
- data/app/assets/images/onlylogs/favicon/apple-touch-icon.png +0 -0
- data/app/assets/images/onlylogs/favicon/favicon-96x96.png +0 -0
- data/app/assets/images/onlylogs/favicon/favicon.ico +0 -0
- data/app/assets/images/onlylogs/favicon/favicon.svg +1 -3
- data/app/assets/images/onlylogs/favicon/web-app-manifest-192x192.png +0 -0
- data/app/assets/images/onlylogs/favicon/web-app-manifest-512x512.png +0 -0
- data/app/assets/images/onlylogs/logo.png +0 -0
- data/app/assets/javascripts/onlylogs/clusterize.js +26 -1
- data/app/channels/onlylogs/logs_channel.rb +14 -9
- data/app/controllers/concerns/onlylogs/authentication.rb +26 -0
- data/app/controllers/onlylogs/application_controller.rb +1 -18
- data/app/controllers/onlylogs/queries_controller.rb +70 -0
- data/app/controllers/onlylogs/viewer_controller.rb +7 -0
- data/app/javascript/onlylogs/controllers/keyboard_shortcuts_controller.js +131 -21
- data/app/javascript/onlylogs/controllers/log_cursor_controller.js +286 -0
- data/app/javascript/onlylogs/controllers/log_streamer_controller.js +42 -2
- data/app/javascript/onlylogs/controllers/match_highlight_controller.js +82 -0
- data/app/javascript/onlylogs/controllers/queries_controller.js +470 -0
- data/app/javascript/onlylogs/controllers/range_slider_controller.js +5 -4
- data/app/javascript/onlylogs/controllers/text_selection_controller.js +136 -15
- data/app/models/onlylogs/file.rb +14 -5
- data/app/models/onlylogs/grep.rb +13 -6
- data/app/models/onlylogs/queries_record.rb +28 -0
- data/app/models/onlylogs/query.rb +29 -0
- data/app/views/onlylogs/logs/index.html.erb +1 -1
- data/app/views/onlylogs/shared/_log_container.html.erb +165 -27
- data/app/views/onlylogs/shared/_log_container_styles.html.erb +729 -44
- data/app/views/onlylogs/shared/_shortcuts_dialog.html.erb +62 -0
- data/bin/super_grep +7 -0
- data/bin/super_ripgrep +5 -0
- data/config/routes.rb +1 -0
- data/lib/onlylogs/configuration.rb +14 -1
- data/lib/onlylogs/http_device.rb +28 -13
- data/lib/onlylogs/version.rb +1 -1
- data/lib/onlylogs.rb +4 -0
- metadata +38 -1
|
@@ -6,12 +6,16 @@ export default class TextSelectionController extends Controller {
|
|
|
6
6
|
connect() {
|
|
7
7
|
this.boundHandleTextSelection = this.handleTextSelection.bind(this)
|
|
8
8
|
this.boundHandleDocumentClick = this.handleDocumentClick.bind(this)
|
|
9
|
+
this.boundHandleDocumentMouseDown = this.handleDocumentMouseDown.bind(this)
|
|
9
10
|
this.boundHandleSelectionChange = this.handleSelectionChange.bind(this)
|
|
10
11
|
this.boundHandleMouseDown = this.handleMouseDown.bind(this)
|
|
12
|
+
this.boundPlaceButton = this.placeButton.bind(this)
|
|
11
13
|
|
|
12
14
|
// Only listen for text selection on the log lines, not the toolbar
|
|
13
15
|
this.logLinesTarget.addEventListener('mousedown', this.boundHandleMouseDown)
|
|
14
16
|
this.logLinesTarget.addEventListener('mouseup', this.boundHandleTextSelection)
|
|
17
|
+
this.logLinesTarget.addEventListener('scroll', this.boundPlaceButton)
|
|
18
|
+
document.addEventListener('mousedown', this.boundHandleDocumentMouseDown)
|
|
15
19
|
document.addEventListener('click', this.boundHandleDocumentClick)
|
|
16
20
|
document.addEventListener('selectionchange', this.boundHandleSelectionChange)
|
|
17
21
|
}
|
|
@@ -19,43 +23,80 @@ export default class TextSelectionController extends Controller {
|
|
|
19
23
|
disconnect() {
|
|
20
24
|
this.logLinesTarget.removeEventListener('mousedown', this.boundHandleMouseDown)
|
|
21
25
|
this.logLinesTarget.removeEventListener('mouseup', this.boundHandleTextSelection)
|
|
26
|
+
this.logLinesTarget.removeEventListener('scroll', this.boundPlaceButton)
|
|
27
|
+
document.removeEventListener('mousedown', this.boundHandleDocumentMouseDown)
|
|
22
28
|
document.removeEventListener('click', this.boundHandleDocumentClick)
|
|
23
29
|
document.removeEventListener('selectionchange', this.boundHandleSelectionChange)
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
handleMouseDown() {
|
|
32
|
+
handleMouseDown(event) {
|
|
33
|
+
if (this.#onScrollbar(event)) return
|
|
34
|
+
|
|
27
35
|
this.dispatch("start")
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
handleDocumentMouseDown(event) {
|
|
39
|
+
if (this.buttonTarget.contains(event.target) || this.#onScrollbar(event)) return
|
|
40
|
+
|
|
41
|
+
this.saved = null
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
handleTextSelection(event) {
|
|
31
45
|
const selection = window.getSelection()
|
|
32
46
|
const selectedText = selection.toString().trim()
|
|
33
|
-
|
|
47
|
+
|
|
34
48
|
// Check if there's actually a text selection
|
|
35
49
|
if (selectedText.length > 0 && selection.rangeCount > 0) {
|
|
36
|
-
this.showButton(event, selectedText)
|
|
50
|
+
this.showButton(event.clientX, event.clientY, selectedText)
|
|
37
51
|
} else {
|
|
38
52
|
this.hideButton()
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
55
|
|
|
42
|
-
|
|
56
|
+
// Bound to log-cursor:select, a selection made with the keyboard.
|
|
57
|
+
showForSelection() {
|
|
58
|
+
const selection = window.getSelection()
|
|
59
|
+
const selectedText = selection.toString().trim()
|
|
60
|
+
if (selectedText.length === 0 || selection.rangeCount === 0) return
|
|
61
|
+
|
|
62
|
+
this.saved = null
|
|
63
|
+
const rect = selection.getRangeAt(0).getBoundingClientRect()
|
|
64
|
+
this.showButton(rect.left + 40, rect.bottom, selectedText)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
showButton(clientX, clientY, selectedText) {
|
|
43
68
|
if (!this.buttonTarget) {
|
|
44
69
|
return
|
|
45
70
|
}
|
|
46
|
-
|
|
71
|
+
|
|
47
72
|
const containerRect = this.element.getBoundingClientRect()
|
|
48
|
-
|
|
73
|
+
|
|
49
74
|
// Position button below mouse cursor
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
this.buttonPosition = {
|
|
76
|
+
left: clientX - containerRect.left - 40, // Roughly center the button
|
|
77
|
+
top: clientY - containerRect.top + 10,
|
|
78
|
+
scrollLeft: this.logLinesTarget.scrollLeft,
|
|
79
|
+
scrollTop: this.logLinesTarget.scrollTop
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
// Store selected text for search
|
|
58
83
|
this.selectedText = selectedText
|
|
84
|
+
this.placeButton()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
placeButton() {
|
|
88
|
+
if (!this.buttonPosition || !this.selectedText) return
|
|
89
|
+
|
|
90
|
+
const lines = this.logLinesTarget
|
|
91
|
+
const left = this.buttonPosition.left - (lines.scrollLeft - this.buttonPosition.scrollLeft)
|
|
92
|
+
const top = this.buttonPosition.top - (lines.scrollTop - this.buttonPosition.scrollTop)
|
|
93
|
+
|
|
94
|
+
const linesTop = lines.getBoundingClientRect().top - this.element.getBoundingClientRect().top
|
|
95
|
+
const visible = top >= linesTop && top <= linesTop + lines.clientHeight - this.buttonTarget.offsetHeight
|
|
96
|
+
|
|
97
|
+
this.buttonTarget.style.left = Math.max(0, left) + 'px'
|
|
98
|
+
this.buttonTarget.style.top = Math.max(0, top) + 'px'
|
|
99
|
+
this.buttonTarget.style.display = visible ? 'block' : 'none'
|
|
59
100
|
}
|
|
60
101
|
|
|
61
102
|
hideButton() {
|
|
@@ -63,6 +104,35 @@ export default class TextSelectionController extends Controller {
|
|
|
63
104
|
this.selectedText = null
|
|
64
105
|
}
|
|
65
106
|
|
|
107
|
+
saveSelection({ detail: { start } }) {
|
|
108
|
+
const selection = window.getSelection()
|
|
109
|
+
if (!this.#selectionInLines(selection)) return
|
|
110
|
+
|
|
111
|
+
const anchor = this.#positionOf(selection.anchorNode, selection.anchorOffset, start)
|
|
112
|
+
const focus = this.#positionOf(selection.focusNode, selection.focusOffset, start)
|
|
113
|
+
if (anchor && focus) this.saved = { anchor, focus }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
restoreSelection({ detail: { start } }) {
|
|
117
|
+
if (!this.saved) return
|
|
118
|
+
|
|
119
|
+
const selection = window.getSelection()
|
|
120
|
+
if (this.#selectionInLines(selection)) return
|
|
121
|
+
|
|
122
|
+
const anchor = this.#pointIn(this.saved.anchor, start)
|
|
123
|
+
const focus = this.#pointIn(this.saved.focus, start)
|
|
124
|
+
if (!anchor || !focus) return
|
|
125
|
+
|
|
126
|
+
selection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset)
|
|
127
|
+
this.selectedText = selection.toString().trim()
|
|
128
|
+
this.placeButton()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
forgetSelection() {
|
|
132
|
+
this.saved = null
|
|
133
|
+
this.hideButton()
|
|
134
|
+
}
|
|
135
|
+
|
|
66
136
|
handleDocumentClick(event) {
|
|
67
137
|
// Only hide if clicking outside the button and outside the log lines container
|
|
68
138
|
if (!this.buttonTarget.contains(event.target) && !this.logLinesTarget.contains(event.target)) {
|
|
@@ -74,7 +144,7 @@ export default class TextSelectionController extends Controller {
|
|
|
74
144
|
// Check if the current selection is still valid
|
|
75
145
|
const selection = window.getSelection()
|
|
76
146
|
const selectedText = selection.toString().trim()
|
|
77
|
-
|
|
147
|
+
|
|
78
148
|
// If no text is selected, hide the button
|
|
79
149
|
if (selectedText.length === 0 || selection.rangeCount === 0) {
|
|
80
150
|
this.hideButton()
|
|
@@ -87,12 +157,63 @@ export default class TextSelectionController extends Controller {
|
|
|
87
157
|
this.regexpModeTarget.checked = false
|
|
88
158
|
this.regexpModeTarget.dispatchEvent(new Event('change', { bubbles: true }))
|
|
89
159
|
}
|
|
90
|
-
|
|
160
|
+
|
|
91
161
|
this.filterInputTarget.value = this.selectedText
|
|
92
162
|
this.filterInputTarget.dispatchEvent(new Event('input', { bubbles: true }))
|
|
93
163
|
this.dispatch("search")
|
|
94
164
|
this.hideButton()
|
|
165
|
+
this.saved = null
|
|
95
166
|
window.getSelection().removeAllRanges()
|
|
96
167
|
}
|
|
97
168
|
}
|
|
169
|
+
|
|
170
|
+
#onScrollbar(event) {
|
|
171
|
+
const lines = this.logLinesTarget
|
|
172
|
+
if (event.target !== lines) return false
|
|
173
|
+
|
|
174
|
+
const rect = lines.getBoundingClientRect()
|
|
175
|
+
return event.clientX - rect.left >= lines.clientWidth || event.clientY - rect.top >= lines.clientHeight
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#selectionInLines(selection) {
|
|
179
|
+
return !selection.isCollapsed && this.#content.contains(selection.anchorNode) && this.#content.contains(selection.focusNode)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#positionOf(node, offset, start) {
|
|
183
|
+
const row = this.#rowFor(node)
|
|
184
|
+
if (!row) return null
|
|
185
|
+
|
|
186
|
+
const range = document.createRange()
|
|
187
|
+
range.setStart(row, 0)
|
|
188
|
+
range.setEnd(node, offset)
|
|
189
|
+
return { row: start + this.#renderedRows().indexOf(row), offset: range.toString().length }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#pointIn(position, start) {
|
|
193
|
+
const row = this.#renderedRows()[position.row - start]
|
|
194
|
+
if (!row) return null
|
|
195
|
+
|
|
196
|
+
const walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT)
|
|
197
|
+
let remaining = position.offset
|
|
198
|
+
let last = null
|
|
199
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
200
|
+
if (remaining <= node.length) return { node, offset: remaining }
|
|
201
|
+
remaining -= node.length
|
|
202
|
+
last = node
|
|
203
|
+
}
|
|
204
|
+
return last ? { node: last, offset: last.length } : { node: row, offset: 0 }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#rowFor(node) {
|
|
208
|
+
while (node && node.parentNode !== this.#content) node = node.parentNode
|
|
209
|
+
return node && !node.classList.contains("clusterize-extra-row") ? node : null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#renderedRows() {
|
|
213
|
+
return [...this.#content.children].filter(row => !row.classList.contains("clusterize-extra-row"))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
get #content() {
|
|
217
|
+
return this.logLinesTarget.querySelector(".clusterize-content")
|
|
218
|
+
}
|
|
98
219
|
}
|
data/app/models/onlylogs/file.rb
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
module Onlylogs
|
|
2
|
-
class Error < StandardError; end
|
|
3
|
-
|
|
4
2
|
class File
|
|
5
3
|
attr_reader :path, :last_position
|
|
6
4
|
|
|
@@ -11,6 +9,16 @@ module Onlylogs
|
|
|
11
9
|
@inode = ::File.stat(path).ino
|
|
12
10
|
end
|
|
13
11
|
|
|
12
|
+
QUERIES_DATABASE = "queries.sqlite3"
|
|
13
|
+
|
|
14
|
+
# The saved queries of this file's directory, shared by all log files in it, newest first.
|
|
15
|
+
def queries = QueriesRecord.bound_to(queries_database_path) { Query.newest_first }
|
|
16
|
+
|
|
17
|
+
# Connects Query to the database of this file's directory.
|
|
18
|
+
def with_database(create: true, &) = QueriesRecord.with_database(queries_database_path, create: create, &)
|
|
19
|
+
|
|
20
|
+
def queries_database_path = ::File.join(::File.dirname(path), Onlylogs.configuration.database_dir, QUERIES_DATABASE)
|
|
21
|
+
|
|
14
22
|
def go_to_position(position)
|
|
15
23
|
return if position < 0
|
|
16
24
|
|
|
@@ -68,10 +76,11 @@ module Onlylogs
|
|
|
68
76
|
|
|
69
77
|
# +timeout+ is in seconds and defaults to nil, meaning unbounded. See
|
|
70
78
|
# Grep.grep.
|
|
71
|
-
def grep(filter, regexp_mode: false, start_position: 0, end_position: nil,
|
|
79
|
+
def grep(filter, regexp_mode: false, case_sensitive: true, start_position: 0, end_position: nil,
|
|
72
80
|
max_matches: Onlylogs.max_line_matches, timeout: nil, &block)
|
|
73
|
-
Grep.grep(filter, path, regexp_mode: regexp_mode,
|
|
74
|
-
end_position: end_position, max_matches: max_matches,
|
|
81
|
+
Grep.grep(filter, path, regexp_mode: regexp_mode, case_sensitive: case_sensitive,
|
|
82
|
+
start_position: start_position, end_position: end_position, max_matches: max_matches,
|
|
83
|
+
timeout: timeout) do |content|
|
|
75
84
|
yield content
|
|
76
85
|
end
|
|
77
86
|
end
|
data/app/models/onlylogs/grep.rb
CHANGED
|
@@ -13,9 +13,10 @@ module Onlylogs
|
|
|
13
13
|
# can afford to hold the thread it runs on. Anything serving a request
|
|
14
14
|
# should pass one.
|
|
15
15
|
def self.grep(pattern, file_path, start_position: 0, end_position: nil, regexp_mode: false,
|
|
16
|
-
max_matches: Onlylogs.max_line_matches, timeout: nil, &block)
|
|
16
|
+
case_sensitive: true, max_matches: Onlylogs.max_line_matches, timeout: nil, &block)
|
|
17
17
|
command_args = search_command(pattern, file_path, start_position: start_position,
|
|
18
|
-
end_position: end_position, regexp_mode: regexp_mode,
|
|
18
|
+
end_position: end_position, regexp_mode: regexp_mode, case_sensitive: case_sensitive,
|
|
19
|
+
max_matches: max_matches)
|
|
19
20
|
|
|
20
21
|
results = []
|
|
21
22
|
|
|
@@ -32,7 +33,7 @@ module Onlylogs
|
|
|
32
33
|
matches = 0
|
|
33
34
|
|
|
34
35
|
ActiveSupport::Notifications.instrument("search.onlylogs", file_path: file_path,
|
|
35
|
-
query: pattern, regexp: regexp_mode, start_position: start_position,
|
|
36
|
+
query: pattern, regexp: regexp_mode, case_sensitive: case_sensitive, start_position: start_position,
|
|
36
37
|
end_position: end_position, max_matches: max_matches) do |payload|
|
|
37
38
|
each_output_line(command_args, timeout: timeout) do |line|
|
|
38
39
|
byte_offset, content = parse_line.call(line.chomp)
|
|
@@ -61,14 +62,18 @@ module Onlylogs
|
|
|
61
62
|
block_given? ? nil : results
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
# The scripts run under LC_ALL=C. ripgrep folds case over Unicode anyway,
|
|
66
|
+
# grep then folds ASCII only: "error" finds "ERROR" with both engines, but
|
|
67
|
+
# "über" finds "Über" with ripgrep only.
|
|
64
68
|
def self.search_command(pattern, file_path, start_position: 0, end_position: nil, regexp_mode: false,
|
|
65
|
-
max_matches: Onlylogs.max_line_matches)
|
|
69
|
+
case_sensitive: true, max_matches: Onlylogs.max_line_matches)
|
|
66
70
|
script_name = Onlylogs.ripgrep_enabled? ? "super_ripgrep" : "super_grep"
|
|
67
71
|
super_grep_path = ::File.expand_path("../../../bin/#{script_name}", __dir__)
|
|
68
72
|
|
|
69
73
|
command_args = [super_grep_path]
|
|
70
74
|
command_args += ["--max-matches", max_matches.to_s] if max_matches.present?
|
|
71
75
|
command_args << "--regexp" if regexp_mode
|
|
76
|
+
command_args << "--case-insensitive" unless case_sensitive
|
|
72
77
|
|
|
73
78
|
# Add byte range parameters if specified
|
|
74
79
|
if start_position > 0 || end_position
|
|
@@ -170,14 +175,16 @@ module Onlylogs
|
|
|
170
175
|
|
|
171
176
|
MATCH_TIMEOUT = 0.1 # seconds
|
|
172
177
|
|
|
173
|
-
|
|
178
|
+
# Folds case over Unicode, like ripgrep. See search_command for grep.
|
|
179
|
+
def self.match_line?(line, string, regexp_mode: false, case_sensitive: true)
|
|
174
180
|
# Strip ANSI color codes from the line before matching
|
|
175
181
|
stripped_line = line.gsub(/\e\[[0-9;]*m/, "")
|
|
176
182
|
# Normalize multiple spaces to single spaces
|
|
177
183
|
normalized_line = stripped_line.gsub(/\s+/, " ")
|
|
178
184
|
|
|
179
185
|
pattern = regexp_mode ? string : Regexp.escape(string)
|
|
180
|
-
|
|
186
|
+
options = case_sensitive ? nil : Regexp::IGNORECASE
|
|
187
|
+
normalized_line.match?(Regexp.new(pattern, options, timeout: MATCH_TIMEOUT))
|
|
181
188
|
rescue ::RegexpError
|
|
182
189
|
false
|
|
183
190
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Onlylogs
|
|
4
|
+
# Saved queries live in a database kept in each log directory, shared by all log files in it:
|
|
5
|
+
# see File#with_database.
|
|
6
|
+
class QueriesRecord < ActiveRecord::Base
|
|
7
|
+
self.abstract_class = true
|
|
8
|
+
include SqliteShards
|
|
9
|
+
|
|
10
|
+
database_migrations(
|
|
11
|
+
->(connection) {
|
|
12
|
+
connection.create_table(:queries, if_not_exists: true) do |t|
|
|
13
|
+
t.string :name, null: false
|
|
14
|
+
t.string :filter, null: false, default: ""
|
|
15
|
+
t.boolean :regexp_mode, null: false, default: false
|
|
16
|
+
t.timestamps
|
|
17
|
+
|
|
18
|
+
t.index "LOWER(name)", unique: true, name: "index_queries_on_lower_name"
|
|
19
|
+
end
|
|
20
|
+
},
|
|
21
|
+
->(connection) {
|
|
22
|
+
unless connection.column_exists?(:queries, :case_insensitive)
|
|
23
|
+
connection.add_column(:queries, :case_insensitive, :boolean, null: false, default: false)
|
|
24
|
+
end
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Onlylogs
|
|
4
|
+
class Query < QueriesRecord
|
|
5
|
+
self.table_name = "queries"
|
|
6
|
+
|
|
7
|
+
scope :newest_first, -> { order(created_at: :desc, id: :desc) }
|
|
8
|
+
|
|
9
|
+
normalizes :name, with: ->(name) { name.strip }
|
|
10
|
+
|
|
11
|
+
normalizes :filter, with: ->(filter) { filter.to_s }, apply_to_nil: true
|
|
12
|
+
normalizes :regexp_mode, with: ->(regexp_mode) { regexp_mode || false }, apply_to_nil: true
|
|
13
|
+
normalizes :case_insensitive, with: ->(case_insensitive) { case_insensitive || false }, apply_to_nil: true
|
|
14
|
+
|
|
15
|
+
validates :name, presence: true, length: {maximum: 255}, uniqueness: {case_sensitive: false}
|
|
16
|
+
validate :filter_must_be_a_valid_regexp
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def filter_must_be_a_valid_regexp
|
|
21
|
+
return unless regexp_mode?
|
|
22
|
+
return if filter.blank?
|
|
23
|
+
|
|
24
|
+
Regexp.new(filter)
|
|
25
|
+
rescue RegexpError => e
|
|
26
|
+
errors.add(:filter, "is not a valid regexp: #{e.message}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
<div class="grid">
|
|
43
43
|
<div class="grid-header">
|
|
44
44
|
<% if @available_log_files.any? %>
|
|
45
|
-
<%= form_with url: root_path, method: :get, local: true, class: "log-file-dropdown", data: { controller: "auto-submit" } do %>
|
|
45
|
+
<%= form_with url: root_path, method: :get, local: true, class: "log-file-dropdown", data: { controller: "auto-submit", onlylogs_shortcut: "o" } do %>
|
|
46
46
|
<select id="log_file_path" name="log_file_path" data-action="change->auto-submit#submit">
|
|
47
47
|
<% @available_log_files.each do |file| %>
|
|
48
48
|
<option value="<%= Onlylogs::SecureFilePath.encrypt(file) %>" <%= "selected" if file.to_s == @log_file_path.to_s %>>
|