log_bench 0.6.1 → 0.7.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/lib/log_bench/app/input_handler.rb +17 -0
- data/lib/log_bench/app/mouse_handler.rb +78 -14
- data/lib/log_bench/app/renderer/ansi.rb +29 -10
- data/lib/log_bench/app/renderer/details.rb +39 -32
- data/lib/log_bench/app/renderer/header.rb +10 -10
- data/lib/log_bench/app/renderer/request_filter_bar.rb +161 -0
- data/lib/log_bench/app/renderer/request_list.rb +63 -47
- data/lib/log_bench/app/renderer/scrollbar.rb +2 -1
- data/lib/log_bench/app/screen.rb +33 -11
- data/lib/log_bench/app/sort.rb +58 -5
- data/lib/log_bench/app/state.rb +135 -16
- data/lib/log_bench/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7db70766181d9cd210a188a6663ebcac7060c7ff1247c94f1fdc6df153c354c0
|
|
4
|
+
data.tar.gz: 2a8a6b811a20ab829d71c14fb7fb2e65c8a83cbe73f77a7901e7f99fac05f751
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '096810750838d2fd337d2f11d91aa71c0be93fdf5dd44893e76025b9a620bd6dd8201e8d08d723e9a68035e8b4ea0e43f5b0e8a7871095c2ffb3871e275758ab'
|
|
7
|
+
data.tar.gz: fa2037148f4bb58d11eb5c018befb646f8f2e2ab658e213a8563df7763b58badef52939673874c0bea375d467235a48ca7899500f05be2f017685619f7015d62
|
|
@@ -16,6 +16,7 @@ module LogBench
|
|
|
16
16
|
CTRL_R = 18 # Undo clear requests (restore)
|
|
17
17
|
ESC = 27 # Escape
|
|
18
18
|
BACKSPACE_KEYS = [127, 8, KEY_BACKSPACE].freeze
|
|
19
|
+
SHIFT_TAB_KEYS = [353, defined?(KEY_BTAB) && KEY_BTAB].compact.uniq.freeze
|
|
19
20
|
EXIT_FILTER_MODE_KEYS = [27, 10, 13].freeze # Escape, Enter, Return
|
|
20
21
|
|
|
21
22
|
# UI constants
|
|
@@ -66,6 +67,10 @@ module LogBench
|
|
|
66
67
|
case ch
|
|
67
68
|
when *EXIT_FILTER_MODE_KEYS
|
|
68
69
|
state.exit_filter_mode
|
|
70
|
+
when KEY_LEFT, *SHIFT_TAB_KEYS
|
|
71
|
+
handle_filter_left_navigation
|
|
72
|
+
when KEY_RIGHT, TAB
|
|
73
|
+
handle_filter_right_navigation
|
|
69
74
|
when KEY_UP
|
|
70
75
|
state.exit_filter_mode
|
|
71
76
|
state.navigate_up
|
|
@@ -79,6 +84,18 @@ module LogBench
|
|
|
79
84
|
end
|
|
80
85
|
end
|
|
81
86
|
|
|
87
|
+
def handle_filter_left_navigation
|
|
88
|
+
if state.filter_mode
|
|
89
|
+
state.previous_request_filter_column
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def handle_filter_right_navigation
|
|
94
|
+
if state.filter_mode
|
|
95
|
+
state.next_request_filter_column
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
82
99
|
def add_character_to_filter(ch)
|
|
83
100
|
return unless printable_character?(ch)
|
|
84
101
|
|
|
@@ -5,9 +5,6 @@ module LogBench
|
|
|
5
5
|
class MouseHandler
|
|
6
6
|
include Curses
|
|
7
7
|
|
|
8
|
-
# UI constants
|
|
9
|
-
DEFAULT_VISIBLE_HEIGHT = 20
|
|
10
|
-
|
|
11
8
|
def initialize(state, screen)
|
|
12
9
|
self.state = state
|
|
13
10
|
self.screen = screen
|
|
@@ -36,6 +33,16 @@ module LogBench
|
|
|
36
33
|
# Switch to left pane if not already focused
|
|
37
34
|
state.switch_to_left_pane unless state.left_pane_focused?
|
|
38
35
|
|
|
36
|
+
if click_on_column_header_row?(y)
|
|
37
|
+
handle_request_header_click(x)
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if click_on_request_filter_row?(y)
|
|
42
|
+
handle_request_filter_click(x)
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
|
|
39
46
|
# Convert click coordinates to request index
|
|
40
47
|
request_index = click_to_request_index(y)
|
|
41
48
|
return unless request_index
|
|
@@ -60,7 +67,7 @@ module LogBench
|
|
|
60
67
|
# Header takes up first HEADER_HEIGHT lines
|
|
61
68
|
# Request list starts at HEADER_HEIGHT + 1 (accounting for border)
|
|
62
69
|
panel_width = screen.panel_width
|
|
63
|
-
header_height =
|
|
70
|
+
header_height = Screen::HEADER_HEIGHT
|
|
64
71
|
|
|
65
72
|
x >= 0 && x < panel_width && y > header_height
|
|
66
73
|
end
|
|
@@ -69,8 +76,8 @@ module LogBench
|
|
|
69
76
|
# Right pane starts after left panel + border width
|
|
70
77
|
# From Screen: panel_width + PANEL_BORDER_WIDTH
|
|
71
78
|
panel_width = screen.panel_width
|
|
72
|
-
border_width =
|
|
73
|
-
header_height =
|
|
79
|
+
border_width = Screen::PANEL_BORDER_WIDTH
|
|
80
|
+
header_height = Screen::HEADER_HEIGHT
|
|
74
81
|
|
|
75
82
|
right_pane_start = panel_width + border_width
|
|
76
83
|
|
|
@@ -78,11 +85,10 @@ module LogBench
|
|
|
78
85
|
end
|
|
79
86
|
|
|
80
87
|
def click_to_request_index(y)
|
|
81
|
-
# Header takes up first 5 lines
|
|
82
|
-
# Request list
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
list_header_offset = 2 # border + column header
|
|
88
|
+
# Header takes up first 5 lines.
|
|
89
|
+
# Request list rows start at RequestList::ROWS_START_Y inside log_win.
|
|
90
|
+
header_height = screen_header_height
|
|
91
|
+
list_header_offset = Renderer::RequestList::ROWS_START_Y
|
|
86
92
|
|
|
87
93
|
row_in_list = y - header_height - list_header_offset
|
|
88
94
|
return nil if row_in_list < 0
|
|
@@ -91,9 +97,67 @@ module LogBench
|
|
|
91
97
|
state.scroll_offset + row_in_list
|
|
92
98
|
end
|
|
93
99
|
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
def click_on_request_filter_row?(y)
|
|
101
|
+
y == screen_header_height + Renderer::RequestList::FILTER_ROW_Y
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def click_on_column_header_row?(y)
|
|
105
|
+
y == screen_header_height + Renderer::RequestList::COLUMN_HEADER_Y
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def handle_request_header_click(x)
|
|
109
|
+
selected_column = request_header_column_for_x(x)
|
|
110
|
+
state.toggle_request_sort(selected_column) if selected_column
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def request_header_column_for_x(x)
|
|
114
|
+
start_x = Renderer::RequestList::HEADER_Y_OFFSET
|
|
115
|
+
method_width = Renderer::RequestList::METHOD_WIDTH
|
|
116
|
+
path_width = screen.panel_width - Renderer::RequestList::PATH_MARGIN
|
|
117
|
+
status_width = Renderer::RequestList::STATUS_WIDTH
|
|
118
|
+
|
|
119
|
+
method_start = start_x
|
|
120
|
+
path_start = method_start + method_width
|
|
121
|
+
status_start = path_start + path_width
|
|
122
|
+
time_start = status_start + status_width
|
|
123
|
+
|
|
124
|
+
ranges = [
|
|
125
|
+
[:method, method_start...(method_start + method_width)],
|
|
126
|
+
[nil, path_start...(path_start + path_width)],
|
|
127
|
+
[:status, status_start...(status_start + status_width)],
|
|
128
|
+
[:time, time_start...screen.panel_width]
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
ranges.each do |column, range|
|
|
132
|
+
return column if range.cover?(x)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def handle_request_filter_click(x)
|
|
139
|
+
selected_column = request_filter_column_for_x(x)
|
|
140
|
+
state.exit_filter_mode
|
|
141
|
+
state.select_request_filter_column(selected_column) if selected_column
|
|
142
|
+
state.enter_filter_mode
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def request_filter_column_for_x(x)
|
|
146
|
+
ranges = Renderer::RequestFilterBar.layout(
|
|
147
|
+
screen.panel_width,
|
|
148
|
+
header_x_offset: Renderer::RequestList::HEADER_Y_OFFSET,
|
|
149
|
+
method_width: Renderer::RequestList::METHOD_WIDTH
|
|
150
|
+
).slice(:method, :path, :status, :time)
|
|
151
|
+
|
|
152
|
+
ranges.each do |column, range|
|
|
153
|
+
return column if range.cover?(x)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def screen_header_height
|
|
160
|
+
Screen::HEADER_HEIGHT
|
|
97
161
|
end
|
|
98
162
|
|
|
99
163
|
def with_warnings_suppressed
|
|
@@ -5,6 +5,25 @@ module LogBench
|
|
|
5
5
|
module Renderer
|
|
6
6
|
class Ansi
|
|
7
7
|
include Curses
|
|
8
|
+
BRIGHT_WHITE = Screen::BRIGHT_WHITE
|
|
9
|
+
BLACK = Screen::BLACK
|
|
10
|
+
ERROR_RED = Screen::ERROR_RED
|
|
11
|
+
SUCCESS_GREEN = Screen::SUCCESS_GREEN
|
|
12
|
+
WARNING_YELLOW = Screen::WARNING_YELLOW
|
|
13
|
+
INFO_BLUE = Screen::INFO_BLUE
|
|
14
|
+
MAGENTA = Screen::MAGENTA
|
|
15
|
+
HEADER_CYAN = Screen::HEADER_CYAN
|
|
16
|
+
|
|
17
|
+
ANSI_RESET = 0
|
|
18
|
+
ANSI_BOLD = 1
|
|
19
|
+
ANSI_BLACK = 30
|
|
20
|
+
ANSI_RED = 31
|
|
21
|
+
ANSI_GREEN = 32
|
|
22
|
+
ANSI_YELLOW = 33
|
|
23
|
+
ANSI_BLUE = 34
|
|
24
|
+
ANSI_MAGENTA = 35
|
|
25
|
+
ANSI_CYAN = 36
|
|
26
|
+
ANSI_WHITE = 37
|
|
8
27
|
|
|
9
28
|
def initialize(screen)
|
|
10
29
|
self.screen = screen
|
|
@@ -166,20 +185,20 @@ module LogBench
|
|
|
166
185
|
|
|
167
186
|
def ansi_to_curses_color(codes)
|
|
168
187
|
# Convert ANSI color codes to curses color pairs
|
|
169
|
-
return nil if codes.empty? || codes == [
|
|
188
|
+
return nil if codes.empty? || codes == [ANSI_RESET]
|
|
170
189
|
|
|
171
190
|
# Handle common ANSI codes
|
|
172
191
|
codes.each do |code|
|
|
173
192
|
case code
|
|
174
|
-
when
|
|
175
|
-
when
|
|
176
|
-
when
|
|
177
|
-
when
|
|
178
|
-
when
|
|
179
|
-
when
|
|
180
|
-
when
|
|
181
|
-
when
|
|
182
|
-
when
|
|
193
|
+
when ANSI_BOLD then return color_pair(BRIGHT_WHITE) | A_BOLD
|
|
194
|
+
when ANSI_BLACK then return color_pair(BLACK)
|
|
195
|
+
when ANSI_RED then return color_pair(ERROR_RED)
|
|
196
|
+
when ANSI_GREEN then return color_pair(SUCCESS_GREEN)
|
|
197
|
+
when ANSI_YELLOW then return color_pair(WARNING_YELLOW)
|
|
198
|
+
when ANSI_BLUE then return color_pair(INFO_BLUE)
|
|
199
|
+
when ANSI_MAGENTA then return color_pair(MAGENTA)
|
|
200
|
+
when ANSI_CYAN then return color_pair(HEADER_CYAN)
|
|
201
|
+
when ANSI_WHITE then return nil
|
|
183
202
|
end
|
|
184
203
|
end
|
|
185
204
|
|
|
@@ -9,6 +9,13 @@ module LogBench
|
|
|
9
9
|
include Curses
|
|
10
10
|
EMPTY_LINE = {text: "", color: nil}
|
|
11
11
|
SEPARATOR_LINE = {text: "", color: nil, separator: true}
|
|
12
|
+
HEADER_CYAN = Screen::HEADER_CYAN
|
|
13
|
+
DEFAULT_WHITE = Screen::DEFAULT_WHITE
|
|
14
|
+
SUCCESS_GREEN = Screen::SUCCESS_GREEN
|
|
15
|
+
WARNING_YELLOW = Screen::WARNING_YELLOW
|
|
16
|
+
INFO_BLUE = Screen::INFO_BLUE
|
|
17
|
+
ERROR_RED = Screen::ERROR_RED
|
|
18
|
+
SELECTION_HIGHLIGHT = Screen::SELECTION_HIGHLIGHT
|
|
12
19
|
|
|
13
20
|
def initialize(screen, state, scrollbar, ansi_renderer)
|
|
14
21
|
self.screen = screen
|
|
@@ -54,9 +61,9 @@ module LogBench
|
|
|
54
61
|
detail_win.setpos(0, 2)
|
|
55
62
|
|
|
56
63
|
if state.right_pane_focused?
|
|
57
|
-
detail_win.attron(color_pair(
|
|
64
|
+
detail_win.attron(color_pair(HEADER_CYAN) | A_BOLD) { detail_win.addstr(" Request Details ") }
|
|
58
65
|
else
|
|
59
|
-
detail_win.attron(color_pair(
|
|
66
|
+
detail_win.attron(color_pair(DEFAULT_WHITE) | A_DIM) { detail_win.addstr(" Request Details ") }
|
|
60
67
|
end
|
|
61
68
|
|
|
62
69
|
# Show detail filter to the right of the title (always visible when active)
|
|
@@ -67,7 +74,7 @@ module LogBench
|
|
|
67
74
|
filter_x = detail_win.maxx - filter_text.length - 3
|
|
68
75
|
if filter_x > 20 # Only show if there's enough space
|
|
69
76
|
detail_win.setpos(0, filter_x)
|
|
70
|
-
detail_win.attron(color_pair(
|
|
77
|
+
detail_win.attron(color_pair(WARNING_YELLOW)) { detail_win.addstr(filter_text) }
|
|
71
78
|
end
|
|
72
79
|
end
|
|
73
80
|
end
|
|
@@ -95,7 +102,7 @@ module LogBench
|
|
|
95
102
|
# Draw highlight background if selected
|
|
96
103
|
if is_selected
|
|
97
104
|
detail_win.setpos(y, 1)
|
|
98
|
-
detail_win.attron(color_pair(
|
|
105
|
+
detail_win.attron(color_pair(SELECTION_HIGHLIGHT) | A_DIM) do
|
|
99
106
|
detail_win.addstr(" " * (detail_win.maxx - 2))
|
|
100
107
|
end
|
|
101
108
|
end
|
|
@@ -106,7 +113,7 @@ module LogBench
|
|
|
106
113
|
if line_data.is_a?(Hash) && line_data[:segments]
|
|
107
114
|
line_data[:segments].each do |segment|
|
|
108
115
|
if is_selected
|
|
109
|
-
detail_win.attron(color_pair(
|
|
116
|
+
detail_win.attron(color_pair(SELECTION_HIGHLIGHT) | A_DIM) { detail_win.addstr(segment[:text]) }
|
|
110
117
|
elsif segment[:color]
|
|
111
118
|
detail_win.attron(segment[:color]) { detail_win.addstr(segment[:text]) }
|
|
112
119
|
else
|
|
@@ -118,14 +125,14 @@ module LogBench
|
|
|
118
125
|
if is_selected
|
|
119
126
|
# For selected ANSI lines, render without ANSI codes to maintain highlight
|
|
120
127
|
plain_text = line_data[:text].gsub(/\e\[[0-9;]*m/, "")
|
|
121
|
-
detail_win.attron(color_pair(
|
|
128
|
+
detail_win.attron(color_pair(SELECTION_HIGHLIGHT) | A_DIM) { detail_win.addstr(plain_text) }
|
|
122
129
|
else
|
|
123
130
|
ansi_renderer.parse_and_render(line_data[:text], detail_win)
|
|
124
131
|
end
|
|
125
132
|
elsif line_data.is_a?(Hash)
|
|
126
133
|
# Handle single-color lines
|
|
127
134
|
if is_selected
|
|
128
|
-
detail_win.attron(color_pair(
|
|
135
|
+
detail_win.attron(color_pair(SELECTION_HIGHLIGHT) | A_DIM) { detail_win.addstr(line_data[:text]) }
|
|
129
136
|
elsif line_data[:color]
|
|
130
137
|
detail_win.attron(line_data[:color]) { detail_win.addstr(line_data[:text]) }
|
|
131
138
|
else
|
|
@@ -133,7 +140,7 @@ module LogBench
|
|
|
133
140
|
end
|
|
134
141
|
elsif is_selected
|
|
135
142
|
# Simple string
|
|
136
|
-
detail_win.attron(color_pair(
|
|
143
|
+
detail_win.attron(color_pair(SELECTION_HIGHLIGHT) | A_DIM) { detail_win.addstr(line_data.to_s) }
|
|
137
144
|
else
|
|
138
145
|
detail_win.addstr(line_data.to_s)
|
|
139
146
|
end
|
|
@@ -164,11 +171,11 @@ module LogBench
|
|
|
164
171
|
|
|
165
172
|
# Method - separate label and value colors
|
|
166
173
|
method_color = case request.method
|
|
167
|
-
when "GET" then color_pair(
|
|
168
|
-
when "POST" then color_pair(
|
|
169
|
-
when "PUT" then color_pair(
|
|
170
|
-
when "DELETE" then color_pair(
|
|
171
|
-
else color_pair(
|
|
174
|
+
when "GET" then color_pair(SUCCESS_GREEN) | A_BOLD
|
|
175
|
+
when "POST" then color_pair(WARNING_YELLOW) | A_BOLD
|
|
176
|
+
when "PUT" then color_pair(INFO_BLUE) | A_BOLD
|
|
177
|
+
when "DELETE" then color_pair(ERROR_RED) | A_BOLD
|
|
178
|
+
else color_pair(DEFAULT_WHITE) | A_BOLD
|
|
172
179
|
end
|
|
173
180
|
|
|
174
181
|
lines << EMPTY_LINE.merge(entry_id: entry_id)
|
|
@@ -178,7 +185,7 @@ module LogBench
|
|
|
178
185
|
color: nil,
|
|
179
186
|
entry_id: entry_id,
|
|
180
187
|
segments: [
|
|
181
|
-
{text: "Method: ", color: color_pair(
|
|
188
|
+
{text: "Method: ", color: color_pair(HEADER_CYAN)},
|
|
182
189
|
{text: request.method, color: method_color}
|
|
183
190
|
]
|
|
184
191
|
}
|
|
@@ -214,7 +221,7 @@ module LogBench
|
|
|
214
221
|
color: nil,
|
|
215
222
|
entry_id: entry_id,
|
|
216
223
|
segments: [
|
|
217
|
-
{text: path_prefix, color: color_pair(
|
|
224
|
+
{text: path_prefix, color: color_pair(HEADER_CYAN)},
|
|
218
225
|
{text: remaining_path, color: nil} # Default white color
|
|
219
226
|
]
|
|
220
227
|
}
|
|
@@ -226,7 +233,7 @@ module LogBench
|
|
|
226
233
|
color: nil,
|
|
227
234
|
entry_id: entry_id,
|
|
228
235
|
segments: [
|
|
229
|
-
{text: path_prefix, color: color_pair(
|
|
236
|
+
{text: path_prefix, color: color_pair(HEADER_CYAN)},
|
|
230
237
|
{text: first_chunk, color: nil} # Default white color
|
|
231
238
|
]
|
|
232
239
|
}
|
|
@@ -245,20 +252,20 @@ module LogBench
|
|
|
245
252
|
if request.status
|
|
246
253
|
# Add status color coding
|
|
247
254
|
status_color = case request.status
|
|
248
|
-
when 200..299 then color_pair(
|
|
249
|
-
when 300..399 then color_pair(
|
|
250
|
-
when 400..599 then color_pair(
|
|
251
|
-
else color_pair(
|
|
255
|
+
when 200..299 then color_pair(SUCCESS_GREEN)
|
|
256
|
+
when 300..399 then color_pair(WARNING_YELLOW)
|
|
257
|
+
when 400..599 then color_pair(ERROR_RED)
|
|
258
|
+
else color_pair(DEFAULT_WHITE)
|
|
252
259
|
end
|
|
253
260
|
|
|
254
261
|
# Build segments for mixed coloring
|
|
255
262
|
segments = [
|
|
256
|
-
{text: "Status: ", color: color_pair(
|
|
263
|
+
{text: "Status: ", color: color_pair(HEADER_CYAN)},
|
|
257
264
|
{text: request.status.to_s, color: status_color}
|
|
258
265
|
]
|
|
259
266
|
|
|
260
267
|
if request.duration
|
|
261
|
-
segments << {text: " | Duration: ", color: color_pair(
|
|
268
|
+
segments << {text: " | Duration: ", color: color_pair(HEADER_CYAN)}
|
|
262
269
|
segments << {text: "#{request.duration}ms", color: nil} # Default white color
|
|
263
270
|
end
|
|
264
271
|
|
|
@@ -280,7 +287,7 @@ module LogBench
|
|
|
280
287
|
color: nil,
|
|
281
288
|
entry_id: entry_id,
|
|
282
289
|
segments: [
|
|
283
|
-
{text: "Controller: ", color: color_pair(
|
|
290
|
+
{text: "Controller: ", color: color_pair(HEADER_CYAN)},
|
|
284
291
|
{text: controller_value, color: nil} # Default white color
|
|
285
292
|
]
|
|
286
293
|
}
|
|
@@ -296,7 +303,7 @@ module LogBench
|
|
|
296
303
|
color: nil,
|
|
297
304
|
entry_id: entry_id,
|
|
298
305
|
segments: [
|
|
299
|
-
{text: "Params:", color: color_pair(
|
|
306
|
+
{text: "Params:", color: color_pair(HEADER_CYAN) | A_BOLD}
|
|
300
307
|
]
|
|
301
308
|
}
|
|
302
309
|
|
|
@@ -369,7 +376,7 @@ module LogBench
|
|
|
369
376
|
color: nil,
|
|
370
377
|
entry_id: entry_id,
|
|
371
378
|
segments: [
|
|
372
|
-
{text: "Request ID: ", color: color_pair(
|
|
379
|
+
{text: "Request ID: ", color: color_pair(HEADER_CYAN)},
|
|
373
380
|
{text: request.request_id, color: nil} # Default white color
|
|
374
381
|
]
|
|
375
382
|
}
|
|
@@ -387,7 +394,7 @@ module LogBench
|
|
|
387
394
|
color: nil,
|
|
388
395
|
entry_id: entry_id,
|
|
389
396
|
segments: [
|
|
390
|
-
{text: "Timestamp: ", color: color_pair(
|
|
397
|
+
{text: "Timestamp: ", color: color_pair(HEADER_CYAN)},
|
|
391
398
|
{text: request.timestamp.strftime("%Y-%m-%d %H:%M:%S UTC"), color: nil} # Default white color
|
|
392
399
|
]
|
|
393
400
|
}
|
|
@@ -415,16 +422,16 @@ module LogBench
|
|
|
415
422
|
|
|
416
423
|
# Show filter status in summary if filtering is active
|
|
417
424
|
summary_title = "Query Summary:"
|
|
418
|
-
lines << {text: summary_title, color: color_pair(
|
|
425
|
+
lines << {text: summary_title, color: color_pair(HEADER_CYAN) | A_BOLD, entry_id: entry_id}
|
|
419
426
|
|
|
420
427
|
if query_stats[:total_queries] > 0
|
|
421
428
|
# Use QuerySummary methods for consistent formatting
|
|
422
429
|
summary_line = query_summary.build_summary_line(query_stats)
|
|
423
|
-
lines << {text: " #{summary_line}", color: color_pair(
|
|
430
|
+
lines << {text: " #{summary_line}", color: color_pair(DEFAULT_WHITE), entry_id: entry_id}
|
|
424
431
|
|
|
425
432
|
breakdown_line = query_summary.build_breakdown_line(query_stats)
|
|
426
433
|
unless breakdown_line.empty?
|
|
427
|
-
lines << {text: " #{breakdown_line}", color: color_pair(
|
|
434
|
+
lines << {text: " #{breakdown_line}", color: color_pair(DEFAULT_WHITE), entry_id: entry_id}
|
|
428
435
|
end
|
|
429
436
|
end
|
|
430
437
|
|
|
@@ -440,13 +447,13 @@ module LogBench
|
|
|
440
447
|
color: nil,
|
|
441
448
|
entry_id: entry_id,
|
|
442
449
|
segments: [
|
|
443
|
-
{text: "Related Logs ", color: color_pair(
|
|
450
|
+
{text: "Related Logs ", color: color_pair(HEADER_CYAN) | A_BOLD},
|
|
444
451
|
{text: count_text, color: A_DIM},
|
|
445
|
-
{text: ":", color: color_pair(
|
|
452
|
+
{text: ":", color: color_pair(HEADER_CYAN) | A_BOLD}
|
|
446
453
|
]
|
|
447
454
|
}
|
|
448
455
|
else
|
|
449
|
-
lines << {text: "Related Logs:", color: color_pair(
|
|
456
|
+
lines << {text: "Related Logs:", color: color_pair(HEADER_CYAN) | A_BOLD, entry_id: entry_id}
|
|
450
457
|
end
|
|
451
458
|
|
|
452
459
|
# Use filtered logs for display - group SQL queries with their call source lines
|
|
@@ -15,8 +15,8 @@ module LogBench
|
|
|
15
15
|
TITLE_X_OFFSET = 2
|
|
16
16
|
|
|
17
17
|
# Color constants
|
|
18
|
-
HEADER_CYAN =
|
|
19
|
-
SUCCESS_GREEN =
|
|
18
|
+
HEADER_CYAN = Screen::HEADER_CYAN
|
|
19
|
+
SUCCESS_GREEN = Screen::SUCCESS_GREEN
|
|
20
20
|
|
|
21
21
|
def initialize(screen, state, log_file_name)
|
|
22
22
|
self.screen = screen
|
|
@@ -50,7 +50,7 @@ module LogBench
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def draw_stats
|
|
53
|
-
if state.
|
|
53
|
+
if state.request_filters_present?
|
|
54
54
|
draw_filtered_stats
|
|
55
55
|
else
|
|
56
56
|
draw_stats_panel
|
|
@@ -63,9 +63,9 @@ module LogBench
|
|
|
63
63
|
total_requests = state.requests.size
|
|
64
64
|
stats_text = "#{filtered_requests.size} found (#{total_requests} total)"
|
|
65
65
|
header_win.setpos(1, screen.width - stats_text.length - 2)
|
|
66
|
-
header_win.attron(color_pair(
|
|
66
|
+
header_win.attron(color_pair(SUCCESS_GREEN)) { header_win.addstr(filtered_requests.size.to_s) }
|
|
67
67
|
header_win.addstr(" found (")
|
|
68
|
-
header_win.attron(color_pair(
|
|
68
|
+
header_win.attron(color_pair(SUCCESS_GREEN)) { header_win.addstr(total_requests.to_s) }
|
|
69
69
|
header_win.addstr(" total)")
|
|
70
70
|
end
|
|
71
71
|
|
|
@@ -120,18 +120,18 @@ module LogBench
|
|
|
120
120
|
header_win.attron(A_DIM) do
|
|
121
121
|
help_line_1 = "a:Auto-scroll("
|
|
122
122
|
header_win.addstr(help_line_1)
|
|
123
|
-
header_win.attron(color_pair(
|
|
124
|
-
header_win.addstr(") | f:Filter | c:Clear filter | s:Sort(")
|
|
125
|
-
header_win.attron(color_pair(
|
|
123
|
+
header_win.attron(color_pair(SUCCESS_GREEN)) { header_win.addstr(state.auto_scroll ? "ON" : "OFF") }
|
|
124
|
+
header_win.addstr(") | f:Filter cols | c:Clear filter | s:Sort(")
|
|
125
|
+
header_win.attron(color_pair(SUCCESS_GREEN)) { header_win.addstr(state.sort.display_name) }
|
|
126
126
|
header_win.addstr(") | t:Text selection(")
|
|
127
|
-
header_win.attron(color_pair(
|
|
127
|
+
header_win.attron(color_pair(SUCCESS_GREEN)) { header_win.addstr(state.text_selection_mode? ? "ON" : "OFF") }
|
|
128
128
|
header_win.addstr(") | q:Quit")
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
header_win.setpos(3, 2)
|
|
132
132
|
header_win.attron(A_DIM) do
|
|
133
133
|
header_win.addstr("←→/hl:Pane | ↑↓/jk:Navigate | g/G:Top/End | y:Copy highlighted | Ctrl+L:Clear | Ctrl+R:Restore(")
|
|
134
|
-
header_win.attron(color_pair(
|
|
134
|
+
header_win.attron(color_pair(SUCCESS_GREEN)) { header_win.addstr(state.can_undo_clear? ? "READY" : "N/A") }
|
|
135
135
|
header_win.addstr(")")
|
|
136
136
|
end
|
|
137
137
|
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogBench
|
|
4
|
+
module App
|
|
5
|
+
module Renderer
|
|
6
|
+
class RequestFilterBar
|
|
7
|
+
include Curses
|
|
8
|
+
|
|
9
|
+
FILTER_HINT_TEXT = "Press f to start filtering, operators allowed: > >= < <= 50-100"
|
|
10
|
+
FILTER_RIGHT_EDGE_OFFSET = 2
|
|
11
|
+
FILTER_STATUS_WIDTH = 7
|
|
12
|
+
FILTER_TIME_WIDTH = 8
|
|
13
|
+
CURSOR_BLINK_INTERVAL_SECONDS = 0.5
|
|
14
|
+
|
|
15
|
+
DEFAULT_WHITE = Screen::DEFAULT_WHITE
|
|
16
|
+
FILTER_CELL_BACKGROUND = Screen::FILTER_CELL_BACKGROUND
|
|
17
|
+
|
|
18
|
+
def initialize(screen, state, header_x_offset:, row_y:, method_width:)
|
|
19
|
+
self.screen = screen
|
|
20
|
+
self.state = state
|
|
21
|
+
self.header_x_offset = header_x_offset
|
|
22
|
+
self.row_y = row_y
|
|
23
|
+
self.method_width = method_width
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def draw
|
|
27
|
+
if show_filter_cells?
|
|
28
|
+
draw_filter_cells_row
|
|
29
|
+
else
|
|
30
|
+
draw_filter_hint_row
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.layout(panel_width, header_x_offset:, method_width:)
|
|
35
|
+
filter_right_edge = panel_width - FILTER_RIGHT_EDGE_OFFSET
|
|
36
|
+
time_start = filter_right_edge - FILTER_TIME_WIDTH
|
|
37
|
+
status_start = time_start - FILTER_STATUS_WIDTH
|
|
38
|
+
path_start = header_x_offset + method_width
|
|
39
|
+
path_width = [status_start - path_start, 1].max
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
method: (header_x_offset...(header_x_offset + method_width)),
|
|
43
|
+
path: (path_start...(path_start + path_width)),
|
|
44
|
+
status: (status_start...(status_start + FILTER_STATUS_WIDTH)),
|
|
45
|
+
time: (time_start...(time_start + FILTER_TIME_WIDTH)),
|
|
46
|
+
path_width: path_width,
|
|
47
|
+
status_start: status_start,
|
|
48
|
+
time_start: time_start,
|
|
49
|
+
filter_right_edge: filter_right_edge
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
attr_accessor :screen, :state, :header_x_offset, :row_y, :method_width
|
|
56
|
+
|
|
57
|
+
def show_filter_cells?
|
|
58
|
+
state.filter_mode || state.request_filters_present?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def draw_filter_hint_row
|
|
62
|
+
log_win.setpos(row_y, header_x_offset)
|
|
63
|
+
consumed = draw_filter_hint_prefix(filter_row_width)
|
|
64
|
+
fill_remaining_filter_row(consumed)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def draw_filter_hint_prefix(width)
|
|
68
|
+
return 0 if width <= 0
|
|
69
|
+
|
|
70
|
+
hint_text = FILTER_HINT_TEXT[0, width].ljust(width)
|
|
71
|
+
log_win.attron(color_pair(DEFAULT_WHITE) | A_DIM) { log_win.addstr(hint_text) }
|
|
72
|
+
hint_text.length
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def fill_remaining_filter_row(consumed_width)
|
|
76
|
+
remaining = filter_row_width - consumed_width
|
|
77
|
+
return if remaining <= 0
|
|
78
|
+
|
|
79
|
+
log_win.attron(color_pair(DEFAULT_WHITE) | A_DIM) { log_win.addstr(" " * remaining) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def draw_filter_cells_row
|
|
83
|
+
current_layout = layout
|
|
84
|
+
|
|
85
|
+
log_win.setpos(row_y, header_x_offset)
|
|
86
|
+
draw_filter_cell(:method, method_width)
|
|
87
|
+
draw_filter_cell(:path, current_layout[:path_width])
|
|
88
|
+
|
|
89
|
+
log_win.setpos(row_y, current_layout[:status_start])
|
|
90
|
+
draw_filter_cell(:status, FILTER_STATUS_WIDTH)
|
|
91
|
+
|
|
92
|
+
log_win.setpos(row_y, current_layout[:time_start])
|
|
93
|
+
draw_filter_cell(:time, FILTER_TIME_WIDTH)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def draw_filter_cell(column, width)
|
|
97
|
+
filter_text = filter_text_for(column, width)
|
|
98
|
+
log_win.attron(filter_cell_attributes(column)) { log_win.addstr(filter_text) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def filter_text_for(column, width)
|
|
102
|
+
filter = state.request_filter_for(column)
|
|
103
|
+
text = filter.display_text.to_s
|
|
104
|
+
text = "#{text}#{active_filter_cursor}" if active_filter_column?(column)
|
|
105
|
+
|
|
106
|
+
align_filter_text(column, text, width)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def align_filter_text(column, text, width)
|
|
110
|
+
if column == :status
|
|
111
|
+
visible_text = text[0, width]
|
|
112
|
+
visible_text.rjust(width - 1).ljust(width)
|
|
113
|
+
elsif column == :time
|
|
114
|
+
visible_text = text[0, width - 1]
|
|
115
|
+
" #{visible_text}".ljust(width)
|
|
116
|
+
else
|
|
117
|
+
text[0, width].ljust(width)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def active_filter_column?(column)
|
|
122
|
+
state.filter_mode && state.active_request_filter_column == column
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def filter_cell_attributes(column)
|
|
126
|
+
base = color_pair(FILTER_CELL_BACKGROUND) | A_DIM
|
|
127
|
+
active_filter_column?(column) ? color_pair(FILTER_CELL_BACKGROUND) : base
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def active_filter_cursor
|
|
131
|
+
cursor_visible? ? "█" : " "
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def cursor_visible?
|
|
135
|
+
blink_tick = (Process.clock_gettime(Process::CLOCK_MONOTONIC) / CURSOR_BLINK_INTERVAL_SECONDS).to_i
|
|
136
|
+
blink_tick.even?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def layout
|
|
140
|
+
self.class.layout(
|
|
141
|
+
screen.panel_width,
|
|
142
|
+
header_x_offset: header_x_offset,
|
|
143
|
+
method_width: method_width
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def filter_row_width
|
|
148
|
+
layout[:filter_right_edge] - header_x_offset
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def color_pair(n)
|
|
152
|
+
screen.color_pair(n)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def log_win
|
|
156
|
+
screen.log_win
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -9,24 +9,36 @@ module LogBench
|
|
|
9
9
|
# Layout constants
|
|
10
10
|
HEADER_Y_OFFSET = 2
|
|
11
11
|
COLUMN_HEADER_Y = 1
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
FILTER_ROW_Y = 2
|
|
13
|
+
ROWS_START_Y = 3
|
|
14
14
|
|
|
15
15
|
# Column widths
|
|
16
16
|
METHOD_WIDTH = 8
|
|
17
17
|
STATUS_WIDTH = 8
|
|
18
|
+
TIME_WIDTH = 6
|
|
19
|
+
STATUS_RENDER_WIDTH = 4
|
|
18
20
|
PATH_MARGIN = 27
|
|
19
21
|
|
|
20
22
|
# Color constants
|
|
21
|
-
HEADER_CYAN =
|
|
22
|
-
DEFAULT_WHITE =
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
HEADER_CYAN = Screen::HEADER_CYAN
|
|
24
|
+
DEFAULT_WHITE = Screen::DEFAULT_WHITE
|
|
25
|
+
SUCCESS_GREEN = Screen::SUCCESS_GREEN
|
|
26
|
+
WARNING_YELLOW = Screen::WARNING_YELLOW
|
|
27
|
+
INFO_BLUE = Screen::INFO_BLUE
|
|
28
|
+
ERROR_RED = Screen::ERROR_RED
|
|
29
|
+
SELECTION_HIGHLIGHT = Screen::SELECTION_HIGHLIGHT
|
|
25
30
|
|
|
26
31
|
def initialize(screen, state, scrollbar)
|
|
27
32
|
self.screen = screen
|
|
28
33
|
self.state = state
|
|
29
34
|
self.scrollbar = scrollbar
|
|
35
|
+
self.filter_bar = RequestFilterBar.new(
|
|
36
|
+
screen,
|
|
37
|
+
state,
|
|
38
|
+
header_x_offset: HEADER_Y_OFFSET,
|
|
39
|
+
row_y: FILTER_ROW_Y,
|
|
40
|
+
method_width: METHOD_WIDTH
|
|
41
|
+
)
|
|
30
42
|
end
|
|
31
43
|
|
|
32
44
|
def draw
|
|
@@ -35,12 +47,13 @@ module LogBench
|
|
|
35
47
|
|
|
36
48
|
draw_header
|
|
37
49
|
draw_column_headers
|
|
50
|
+
draw_filter_row
|
|
38
51
|
draw_rows
|
|
39
52
|
end
|
|
40
53
|
|
|
41
54
|
private
|
|
42
55
|
|
|
43
|
-
attr_accessor :screen, :state, :scrollbar
|
|
56
|
+
attr_accessor :screen, :state, :scrollbar, :filter_bar
|
|
44
57
|
|
|
45
58
|
def draw_header
|
|
46
59
|
log_win.setpos(0, HEADER_Y_OFFSET)
|
|
@@ -50,37 +63,30 @@ module LogBench
|
|
|
50
63
|
else
|
|
51
64
|
log_win.attron(color_pair(DEFAULT_WHITE) | A_DIM) { log_win.addstr(" Request Logs ") }
|
|
52
65
|
end
|
|
53
|
-
|
|
54
|
-
show_filter_in_header if show_filter?
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def show_filter?
|
|
58
|
-
state.main_filter.present? || state.main_filter.active?
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def show_filter_in_header
|
|
62
|
-
filter_text = "Filter: #{state.main_filter.cursor_display}"
|
|
63
|
-
filter_x = log_win.maxx - filter_text.length - FILTER_X_MARGIN
|
|
64
|
-
|
|
65
|
-
if filter_x > MIN_FILTER_X_POSITION
|
|
66
|
-
log_win.setpos(0, filter_x)
|
|
67
|
-
log_win.attron(color_pair(WARNING_YELLOW)) { log_win.addstr(filter_text) }
|
|
68
|
-
end
|
|
69
66
|
end
|
|
70
67
|
|
|
71
68
|
def draw_column_headers
|
|
72
69
|
log_win.setpos(COLUMN_HEADER_Y, HEADER_Y_OFFSET)
|
|
73
70
|
log_win.attron(color_pair(HEADER_CYAN) | A_DIM) do
|
|
74
|
-
log_win.addstr("METHOD".ljust(METHOD_WIDTH))
|
|
75
|
-
log_win.addstr("PATH".ljust(
|
|
76
|
-
log_win.addstr("STATUS".ljust(STATUS_WIDTH))
|
|
77
|
-
log_win.addstr("TIME")
|
|
71
|
+
log_win.addstr(column_header_text("METHOD", :method).ljust(METHOD_WIDTH))
|
|
72
|
+
log_win.addstr("PATH".ljust(path_column_width))
|
|
73
|
+
log_win.addstr(column_header_text("STATUS", :status).ljust(STATUS_WIDTH))
|
|
74
|
+
log_win.addstr(column_header_text("TIME", :time))
|
|
78
75
|
end
|
|
79
76
|
end
|
|
80
77
|
|
|
78
|
+
def column_header_text(label, column)
|
|
79
|
+
sort_arrow = state.sort_arrow_for_column(column)
|
|
80
|
+
sort_arrow ? "#{label}#{sort_arrow}" : label
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def draw_filter_row
|
|
84
|
+
filter_bar.draw
|
|
85
|
+
end
|
|
86
|
+
|
|
81
87
|
def draw_rows
|
|
82
88
|
filtered_requests = state.filtered_requests
|
|
83
|
-
visible_height = log_win.maxy -
|
|
89
|
+
visible_height = log_win.maxy - 4
|
|
84
90
|
|
|
85
91
|
return draw_no_requests_message if filtered_requests.empty?
|
|
86
92
|
|
|
@@ -91,7 +97,7 @@ module LogBench
|
|
|
91
97
|
request_index = state.scroll_offset + i
|
|
92
98
|
break if request_index >= filtered_requests.size
|
|
93
99
|
|
|
94
|
-
draw_row(filtered_requests[request_index], request_index, i +
|
|
100
|
+
draw_row(filtered_requests[request_index], request_index, i + ROWS_START_Y)
|
|
95
101
|
end
|
|
96
102
|
|
|
97
103
|
# Draw scrollbar if needed
|
|
@@ -110,7 +116,7 @@ module LogBench
|
|
|
110
116
|
is_selected = request_index == state.selected
|
|
111
117
|
|
|
112
118
|
if is_selected
|
|
113
|
-
log_win.attron(color_pair(
|
|
119
|
+
log_win.attron(color_pair(SELECTION_HIGHLIGHT) | A_DIM) do
|
|
114
120
|
log_win.addstr(" " * (screen.panel_width - 4))
|
|
115
121
|
end
|
|
116
122
|
log_win.setpos(y_position, 1)
|
|
@@ -126,7 +132,7 @@ module LogBench
|
|
|
126
132
|
method_text = " #{request.method.ljust(7)} "
|
|
127
133
|
|
|
128
134
|
if is_selected
|
|
129
|
-
log_win.attron(color_pair(
|
|
135
|
+
log_win.attron(color_pair(SELECTION_HIGHLIGHT) | A_DIM) { log_win.addstr(method_text) }
|
|
130
136
|
else
|
|
131
137
|
method_color = method_color_for(request.method)
|
|
132
138
|
log_win.attron(color_pair(method_color) | A_BOLD) { log_win.addstr(method_text) }
|
|
@@ -134,12 +140,12 @@ module LogBench
|
|
|
134
140
|
end
|
|
135
141
|
|
|
136
142
|
def draw_path_column(request, is_selected)
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
path = request.path[0, path_column_width] || ""
|
|
144
|
+
path_width = path_column_width
|
|
139
145
|
path_text = path.ljust(path_width)
|
|
140
146
|
|
|
141
147
|
if is_selected
|
|
142
|
-
log_win.attron(color_pair(
|
|
148
|
+
log_win.attron(color_pair(SELECTION_HIGHLIGHT) | A_DIM) { log_win.addstr(path_text) }
|
|
143
149
|
else
|
|
144
150
|
log_win.addstr(path_text)
|
|
145
151
|
end
|
|
@@ -148,12 +154,11 @@ module LogBench
|
|
|
148
154
|
def draw_status_column(request, is_selected)
|
|
149
155
|
return unless request.status
|
|
150
156
|
|
|
151
|
-
status_col_start = screen.panel_width - 14
|
|
152
157
|
status_text = "#{request.status.to_s.rjust(3)} "
|
|
153
158
|
|
|
154
159
|
log_win.setpos(log_win.cury, status_col_start)
|
|
155
160
|
if is_selected
|
|
156
|
-
log_win.attron(color_pair(
|
|
161
|
+
log_win.attron(color_pair(SELECTION_HIGHLIGHT) | A_DIM) { log_win.addstr(status_text) }
|
|
157
162
|
else
|
|
158
163
|
status_color = status_color_for(request.status)
|
|
159
164
|
log_win.attron(color_pair(status_color)) { log_win.addstr(status_text) }
|
|
@@ -163,12 +168,11 @@ module LogBench
|
|
|
163
168
|
def draw_duration_column(request, is_selected)
|
|
164
169
|
return unless request.duration
|
|
165
170
|
|
|
166
|
-
duration_col_start = screen.panel_width - 9
|
|
167
171
|
duration_text = "#{request.duration.to_i}ms".ljust(6) + " "
|
|
168
172
|
|
|
169
173
|
log_win.setpos(log_win.cury, duration_col_start)
|
|
170
174
|
if is_selected
|
|
171
|
-
log_win.attron(color_pair(
|
|
175
|
+
log_win.attron(color_pair(SELECTION_HIGHLIGHT) | A_DIM) { log_win.addstr(duration_text) }
|
|
172
176
|
else
|
|
173
177
|
log_win.attron(A_DIM) { log_win.addstr(duration_text) }
|
|
174
178
|
end
|
|
@@ -176,23 +180,35 @@ module LogBench
|
|
|
176
180
|
|
|
177
181
|
def method_color_for(method)
|
|
178
182
|
case method
|
|
179
|
-
when "GET" then
|
|
180
|
-
when "POST" then
|
|
181
|
-
when "PUT" then
|
|
182
|
-
when "DELETE" then
|
|
183
|
-
else
|
|
183
|
+
when "GET" then SUCCESS_GREEN
|
|
184
|
+
when "POST" then WARNING_YELLOW
|
|
185
|
+
when "PUT" then INFO_BLUE
|
|
186
|
+
when "DELETE" then ERROR_RED
|
|
187
|
+
else DEFAULT_WHITE
|
|
184
188
|
end
|
|
185
189
|
end
|
|
186
190
|
|
|
187
191
|
def status_color_for(status)
|
|
188
192
|
case status
|
|
189
|
-
when 200..299 then
|
|
190
|
-
when 300..399 then
|
|
191
|
-
when 400..599 then
|
|
192
|
-
else
|
|
193
|
+
when 200..299 then SUCCESS_GREEN
|
|
194
|
+
when 300..399 then WARNING_YELLOW
|
|
195
|
+
when 400..599 then ERROR_RED
|
|
196
|
+
else DEFAULT_WHITE
|
|
193
197
|
end
|
|
194
198
|
end
|
|
195
199
|
|
|
200
|
+
def path_column_width
|
|
201
|
+
screen.panel_width - PATH_MARGIN
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def status_col_start
|
|
205
|
+
screen.panel_width - 14
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def duration_col_start
|
|
209
|
+
screen.panel_width - 9
|
|
210
|
+
end
|
|
211
|
+
|
|
196
212
|
def color_pair(n)
|
|
197
213
|
screen.color_pair(n)
|
|
198
214
|
end
|
|
@@ -5,6 +5,7 @@ module LogBench
|
|
|
5
5
|
module Renderer
|
|
6
6
|
class Scrollbar
|
|
7
7
|
include Curses
|
|
8
|
+
SCROLLBAR_THUMB_COLOR = Screen::HEADER_CYAN
|
|
8
9
|
|
|
9
10
|
def initialize(screen)
|
|
10
11
|
self.screen = screen
|
|
@@ -20,7 +21,7 @@ module LogBench
|
|
|
20
21
|
height.times do |i|
|
|
21
22
|
win.setpos(i + 1, x)
|
|
22
23
|
if i >= scrollbar_pos && i < scrollbar_pos + scrollbar_height
|
|
23
|
-
win.attron(color_pair(
|
|
24
|
+
win.attron(color_pair(SCROLLBAR_THUMB_COLOR)) { win.addstr("█") } # Solid block for scrollbar thumb
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
end
|
data/lib/log_bench/app/screen.rb
CHANGED
|
@@ -9,8 +9,11 @@ module LogBench
|
|
|
9
9
|
HEADER_HEIGHT = 5
|
|
10
10
|
PANEL_BORDER_WIDTH = 3
|
|
11
11
|
INPUT_TIMEOUT_MS = 200
|
|
12
|
+
TRANSPARENT_BACKGROUND = -1
|
|
13
|
+
EXTENDED_COLOR_THRESHOLD = 253
|
|
14
|
+
NO_COLOR_SUPPORT = 0
|
|
12
15
|
|
|
13
|
-
# Color pairs
|
|
16
|
+
# Color pairs (identifiers)
|
|
14
17
|
HEADER_CYAN = 1
|
|
15
18
|
DEFAULT_WHITE = 2
|
|
16
19
|
SUCCESS_GREEN = 3 # GET requests, 200 status
|
|
@@ -21,6 +24,7 @@ module LogBench
|
|
|
21
24
|
BLACK = 8
|
|
22
25
|
MAGENTA = 9
|
|
23
26
|
SELECTION_HIGHLIGHT = 10
|
|
27
|
+
FILTER_CELL_BACKGROUND = 11
|
|
24
28
|
|
|
25
29
|
attr_reader :header_win, :log_win, :panel_width, :detail_win
|
|
26
30
|
|
|
@@ -89,17 +93,35 @@ module LogBench
|
|
|
89
93
|
stdscr.keypad(true)
|
|
90
94
|
stdscr.timeout = INPUT_TIMEOUT_MS
|
|
91
95
|
|
|
92
|
-
# Define color pairs with transparent background
|
|
93
|
-
init_pair(HEADER_CYAN, COLOR_CYAN,
|
|
94
|
-
init_pair(DEFAULT_WHITE, COLOR_WHITE,
|
|
95
|
-
init_pair(SUCCESS_GREEN, COLOR_GREEN,
|
|
96
|
-
init_pair(WARNING_YELLOW, COLOR_YELLOW,
|
|
97
|
-
init_pair(INFO_BLUE, COLOR_BLUE,
|
|
98
|
-
init_pair(ERROR_RED, COLOR_RED,
|
|
99
|
-
init_pair(BRIGHT_WHITE, COLOR_WHITE,
|
|
100
|
-
init_pair(BLACK, COLOR_BLACK,
|
|
101
|
-
init_pair(MAGENTA, COLOR_MAGENTA,
|
|
96
|
+
# Define color pairs with transparent background.
|
|
97
|
+
init_pair(HEADER_CYAN, COLOR_CYAN, TRANSPARENT_BACKGROUND) # Header/Cyan
|
|
98
|
+
init_pair(DEFAULT_WHITE, COLOR_WHITE, TRANSPARENT_BACKGROUND) # Default/White
|
|
99
|
+
init_pair(SUCCESS_GREEN, COLOR_GREEN, TRANSPARENT_BACKGROUND) # GET/Success/Green
|
|
100
|
+
init_pair(WARNING_YELLOW, COLOR_YELLOW, TRANSPARENT_BACKGROUND) # POST/Warning/Yellow
|
|
101
|
+
init_pair(INFO_BLUE, COLOR_BLUE, TRANSPARENT_BACKGROUND) # PUT/Blue
|
|
102
|
+
init_pair(ERROR_RED, COLOR_RED, TRANSPARENT_BACKGROUND) # DELETE/Error/Red
|
|
103
|
+
init_pair(BRIGHT_WHITE, COLOR_WHITE, TRANSPARENT_BACKGROUND) # Bold/Bright white
|
|
104
|
+
init_pair(BLACK, COLOR_BLACK, TRANSPARENT_BACKGROUND) # Black
|
|
105
|
+
init_pair(MAGENTA, COLOR_MAGENTA, TRANSPARENT_BACKGROUND) # Magenta
|
|
102
106
|
init_pair(SELECTION_HIGHLIGHT, COLOR_BLACK, COLOR_CYAN) # Selection highlighting
|
|
107
|
+
if terminal_colors_count >= EXTENDED_COLOR_THRESHOLD
|
|
108
|
+
# Keep filter cell text readable on rich-color terminals.
|
|
109
|
+
init_pair(FILTER_CELL_BACKGROUND, COLOR_YELLOW, TRANSPARENT_BACKGROUND)
|
|
110
|
+
else
|
|
111
|
+
init_pair(FILTER_CELL_BACKGROUND, COLOR_BLACK, COLOR_WHITE)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def terminal_colors_count
|
|
116
|
+
if Curses.respond_to?(:colors)
|
|
117
|
+
Curses.colors.to_i
|
|
118
|
+
elsif defined?(COLORS)
|
|
119
|
+
COLORS.to_i
|
|
120
|
+
else
|
|
121
|
+
NO_COLOR_SUPPORT
|
|
122
|
+
end
|
|
123
|
+
rescue
|
|
124
|
+
NO_COLOR_SUPPORT
|
|
103
125
|
end
|
|
104
126
|
|
|
105
127
|
def cleanup_windows
|
data/lib/log_bench/app/sort.rb
CHANGED
|
@@ -2,39 +2,83 @@ module LogBench
|
|
|
2
2
|
module App
|
|
3
3
|
class Sort
|
|
4
4
|
MODES = [:timestamp, :duration, :method, :status].freeze
|
|
5
|
+
REQUEST_COLUMN_TO_MODE = {
|
|
6
|
+
method: :method,
|
|
7
|
+
status: :status,
|
|
8
|
+
time: :duration
|
|
9
|
+
}.freeze
|
|
10
|
+
DEFAULT_DIRECTIONS = {
|
|
11
|
+
timestamp: :asc,
|
|
12
|
+
duration: :desc,
|
|
13
|
+
method: :asc,
|
|
14
|
+
status: :desc
|
|
15
|
+
}.freeze
|
|
16
|
+
ASC_ARROW = "↑"
|
|
17
|
+
DESC_ARROW = "↓"
|
|
5
18
|
|
|
6
19
|
def initialize
|
|
7
20
|
self.mode = :timestamp
|
|
21
|
+
self.direction = DEFAULT_DIRECTIONS.fetch(mode)
|
|
8
22
|
end
|
|
9
23
|
|
|
10
24
|
def cycle
|
|
11
25
|
current_index = MODES.index(mode)
|
|
12
26
|
next_index = (current_index + 1) % MODES.length
|
|
13
27
|
self.mode = MODES[next_index]
|
|
28
|
+
self.direction = DEFAULT_DIRECTIONS.fetch(mode)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def toggle_column(column)
|
|
32
|
+
target_mode = REQUEST_COLUMN_TO_MODE[column]
|
|
33
|
+
return false unless target_mode
|
|
34
|
+
|
|
35
|
+
if mode == target_mode
|
|
36
|
+
if direction == DEFAULT_DIRECTIONS.fetch(mode)
|
|
37
|
+
toggle_direction
|
|
38
|
+
else
|
|
39
|
+
reset_to_default_sort
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
self.mode = target_mode
|
|
43
|
+
self.direction = DEFAULT_DIRECTIONS.fetch(mode)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def sort_arrow_for_column(column)
|
|
50
|
+
target_mode = REQUEST_COLUMN_TO_MODE[column]
|
|
51
|
+
return nil unless target_mode == mode
|
|
52
|
+
|
|
53
|
+
(direction == :asc) ? ASC_ARROW : DESC_ARROW
|
|
14
54
|
end
|
|
15
55
|
|
|
16
56
|
def display_name
|
|
17
|
-
case mode
|
|
57
|
+
mode_name = case mode
|
|
18
58
|
when :timestamp then "TIMESTAMP"
|
|
19
59
|
when :duration then "DURATION"
|
|
20
60
|
when :method then "METHOD"
|
|
21
61
|
when :status then "STATUS"
|
|
22
62
|
end
|
|
63
|
+
|
|
64
|
+
"#{mode_name} #{direction.to_s.upcase}"
|
|
23
65
|
end
|
|
24
66
|
|
|
25
67
|
def sort_requests(requests)
|
|
26
|
-
case mode
|
|
68
|
+
sorted = case mode
|
|
27
69
|
when :timestamp
|
|
28
70
|
requests.sort_by { |req| req.timestamp || Time.at(0) }
|
|
29
71
|
when :duration
|
|
30
|
-
requests.sort_by { |req|
|
|
72
|
+
requests.sort_by { |req| req.duration || 0 }
|
|
31
73
|
when :method
|
|
32
74
|
requests.sort_by { |req| req.method || "" }
|
|
33
75
|
when :status
|
|
34
|
-
requests.sort_by { |req|
|
|
76
|
+
requests.sort_by { |req| req.status || 0 }
|
|
35
77
|
else
|
|
36
78
|
requests
|
|
37
79
|
end
|
|
80
|
+
|
|
81
|
+
(direction == :desc) ? sorted.reverse : sorted
|
|
38
82
|
end
|
|
39
83
|
|
|
40
84
|
def timestamp?
|
|
@@ -55,7 +99,16 @@ module LogBench
|
|
|
55
99
|
|
|
56
100
|
private
|
|
57
101
|
|
|
58
|
-
attr_accessor :mode
|
|
102
|
+
attr_accessor :mode, :direction
|
|
103
|
+
|
|
104
|
+
def toggle_direction
|
|
105
|
+
self.direction = (direction == :asc) ? :desc : :asc
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def reset_to_default_sort
|
|
109
|
+
self.mode = :timestamp
|
|
110
|
+
self.direction = DEFAULT_DIRECTIONS.fetch(mode)
|
|
111
|
+
end
|
|
59
112
|
end
|
|
60
113
|
end
|
|
61
114
|
end
|
data/lib/log_bench/app/state.rb
CHANGED
|
@@ -7,7 +7,14 @@ module LogBench
|
|
|
7
7
|
class State
|
|
8
8
|
include Singleton
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
REQUEST_FILTER_COLUMNS = %i[method path status time].freeze
|
|
11
|
+
NUMERIC_COMPARATOR_REGEX = /\A(<=|>=|<|>)\s*(-?\d+(?:\.\d+)?)\z/
|
|
12
|
+
NUMERIC_COMPARATOR_ONLY_REGEX = /\A(<=|>=|<|>)\s*\z/
|
|
13
|
+
NUMERIC_RANGE_REGEX = /\A(-?\d+(?:\.\d+)?)\s*-\s*(-?\d+(?:\.\d+)?)\z/
|
|
14
|
+
NUMERIC_VALUE_REGEX = /\A-?\d+(?:\.\d+)?\z/
|
|
15
|
+
NUMERIC_COMPARISON_EPSILON = 0.0001
|
|
16
|
+
|
|
17
|
+
attr_reader :main_filter, :sort, :detail_filter, :cleared_requests, :start_time, :stats, :total_queries, :active_request_filter_column
|
|
11
18
|
attr_accessor :requests, :orphan_requests, :auto_scroll, :scroll_offset, :selected, :detail_scroll_offset, :detail_selected_entry, :text_selection_mode, :update_available, :update_version
|
|
12
19
|
|
|
13
20
|
def initialize
|
|
@@ -27,6 +34,8 @@ module LogBench
|
|
|
27
34
|
self.text_selection_mode = false
|
|
28
35
|
self.main_filter = Filter.new
|
|
29
36
|
self.detail_filter = Filter.new
|
|
37
|
+
self.request_filters = build_request_filters
|
|
38
|
+
self.active_request_filter_column = :path
|
|
30
39
|
self.sort = Sort.new
|
|
31
40
|
self.update_available = false
|
|
32
41
|
self.update_version = nil
|
|
@@ -81,6 +90,7 @@ module LogBench
|
|
|
81
90
|
|
|
82
91
|
def clear_requests_filter
|
|
83
92
|
main_filter.clear
|
|
93
|
+
request_filters.each_value(&:clear)
|
|
84
94
|
self.selected = 0
|
|
85
95
|
self.scroll_offset = 0
|
|
86
96
|
end
|
|
@@ -137,6 +147,18 @@ module LogBench
|
|
|
137
147
|
sort.cycle
|
|
138
148
|
end
|
|
139
149
|
|
|
150
|
+
def toggle_request_sort(column)
|
|
151
|
+
return unless sort.toggle_column(column)
|
|
152
|
+
|
|
153
|
+
self.auto_scroll = false
|
|
154
|
+
self.selected = 0
|
|
155
|
+
self.scroll_offset = 0
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def sort_arrow_for_column(column)
|
|
159
|
+
sort.sort_arrow_for_column(column)
|
|
160
|
+
end
|
|
161
|
+
|
|
140
162
|
def switch_to_left_pane
|
|
141
163
|
self.focused_pane = :left
|
|
142
164
|
end
|
|
@@ -156,6 +178,7 @@ module LogBench
|
|
|
156
178
|
def enter_filter_mode
|
|
157
179
|
if left_pane_focused?
|
|
158
180
|
main_filter.enter_mode
|
|
181
|
+
self.active_request_filter_column ||= :path
|
|
159
182
|
else
|
|
160
183
|
detail_filter.enter_mode
|
|
161
184
|
end
|
|
@@ -168,7 +191,7 @@ module LogBench
|
|
|
168
191
|
|
|
169
192
|
def add_to_filter(char)
|
|
170
193
|
if main_filter.active?
|
|
171
|
-
|
|
194
|
+
active_request_filter.add_character(char)
|
|
172
195
|
elsif detail_filter.active?
|
|
173
196
|
detail_filter.add_character(char)
|
|
174
197
|
end
|
|
@@ -176,7 +199,7 @@ module LogBench
|
|
|
176
199
|
|
|
177
200
|
def backspace_filter
|
|
178
201
|
if main_filter.active?
|
|
179
|
-
|
|
202
|
+
active_request_filter.remove_character
|
|
180
203
|
elsif detail_filter.active?
|
|
181
204
|
detail_filter.remove_character
|
|
182
205
|
end
|
|
@@ -190,19 +213,34 @@ module LogBench
|
|
|
190
213
|
detail_filter.active?
|
|
191
214
|
end
|
|
192
215
|
|
|
216
|
+
def request_filter_columns
|
|
217
|
+
REQUEST_FILTER_COLUMNS
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def request_filter_for(column)
|
|
221
|
+
request_filters[column]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def select_request_filter_column(column)
|
|
225
|
+
return unless request_filter_columns.include?(column)
|
|
226
|
+
|
|
227
|
+
self.active_request_filter_column = column
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def next_request_filter_column
|
|
231
|
+
switch_request_filter_column(1)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def previous_request_filter_column
|
|
235
|
+
switch_request_filter_column(-1)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def request_filters_present?
|
|
239
|
+
request_filters.values.any?(&:present?) || main_filter.present?
|
|
240
|
+
end
|
|
241
|
+
|
|
193
242
|
def filtered_requests
|
|
194
|
-
filtered =
|
|
195
|
-
requests.select do |req|
|
|
196
|
-
main_filter.matches?(req.path) ||
|
|
197
|
-
main_filter.matches?(req.method) ||
|
|
198
|
-
main_filter.matches?(req.controller) ||
|
|
199
|
-
main_filter.matches?(req.action) ||
|
|
200
|
-
main_filter.matches?(req.status) ||
|
|
201
|
-
main_filter.matches?(req.request_id)
|
|
202
|
-
end
|
|
203
|
-
else
|
|
204
|
-
requests
|
|
205
|
-
end
|
|
243
|
+
filtered = requests.select { |req| request_matches_filters?(req) }
|
|
206
244
|
|
|
207
245
|
sort.sort_requests(filtered)
|
|
208
246
|
end
|
|
@@ -336,8 +374,89 @@ module LogBench
|
|
|
336
374
|
|
|
337
375
|
private
|
|
338
376
|
|
|
377
|
+
attr_reader :request_filters
|
|
339
378
|
attr_accessor :focused_pane, :running, :job_ids_map
|
|
340
|
-
attr_writer :main_filter, :detail_filter, :sort, :cleared_requests, :start_time, :stats, :total_queries
|
|
379
|
+
attr_writer :main_filter, :detail_filter, :sort, :cleared_requests, :start_time, :stats, :total_queries, :request_filters, :active_request_filter_column
|
|
380
|
+
|
|
381
|
+
def build_request_filters
|
|
382
|
+
REQUEST_FILTER_COLUMNS.to_h { |column| [column, Filter.new] }
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def active_request_filter
|
|
386
|
+
request_filter_for(active_request_filter_column) || request_filter_for(:path)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def switch_request_filter_column(direction)
|
|
390
|
+
return unless request_filter_columns.include?(active_request_filter_column)
|
|
391
|
+
|
|
392
|
+
current_index = request_filter_columns.index(active_request_filter_column)
|
|
393
|
+
next_index = (current_index + direction) % request_filter_columns.length
|
|
394
|
+
self.active_request_filter_column = request_filter_columns[next_index]
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def request_matches_filters?(request)
|
|
398
|
+
matches_legacy_main_filter?(request) && matches_column_filters?(request)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def matches_legacy_main_filter?(request)
|
|
402
|
+
return true unless main_filter.present?
|
|
403
|
+
|
|
404
|
+
main_filter.matches?(request.path) ||
|
|
405
|
+
main_filter.matches?(request.method) ||
|
|
406
|
+
main_filter.matches?(request.controller) ||
|
|
407
|
+
main_filter.matches?(request.action) ||
|
|
408
|
+
main_filter.matches?(request.status) ||
|
|
409
|
+
main_filter.matches?(request.request_id)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def matches_column_filters?(request)
|
|
413
|
+
request_filters.all? do |column, filter|
|
|
414
|
+
next true unless filter.present?
|
|
415
|
+
|
|
416
|
+
case column
|
|
417
|
+
when :method
|
|
418
|
+
filter.matches?(request.method)
|
|
419
|
+
when :path
|
|
420
|
+
filter.matches?(request.path)
|
|
421
|
+
when :status
|
|
422
|
+
numeric_filter_matches?(request.status, filter.display_text)
|
|
423
|
+
when :time
|
|
424
|
+
numeric_filter_matches?(request.duration, filter.display_text)
|
|
425
|
+
else
|
|
426
|
+
true
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def numeric_filter_matches?(value, filter_text)
|
|
432
|
+
return true if filter_text.nil?
|
|
433
|
+
|
|
434
|
+
expression = filter_text.strip
|
|
435
|
+
return true if expression.empty?
|
|
436
|
+
return true if expression.match?(NUMERIC_COMPARATOR_ONLY_REGEX)
|
|
437
|
+
return false if value.nil?
|
|
438
|
+
|
|
439
|
+
if (comparison = expression.match(NUMERIC_COMPARATOR_REGEX))
|
|
440
|
+
compare_numeric(value.to_f, comparison[1], comparison[2].to_f)
|
|
441
|
+
elsif (range = expression.match(NUMERIC_RANGE_REGEX))
|
|
442
|
+
min_value, max_value = [range[1].to_f, range[2].to_f].minmax
|
|
443
|
+
value.to_f.between?(min_value, max_value)
|
|
444
|
+
elsif expression.match?(NUMERIC_VALUE_REGEX)
|
|
445
|
+
(value.to_f - expression.to_f).abs <= NUMERIC_COMPARISON_EPSILON
|
|
446
|
+
else
|
|
447
|
+
value.to_s.downcase.include?(expression.downcase)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def compare_numeric(value, operator, threshold)
|
|
452
|
+
case operator
|
|
453
|
+
when "<" then value < threshold
|
|
454
|
+
when "<=" then value <= threshold
|
|
455
|
+
when ">" then value > threshold
|
|
456
|
+
when ">=" then value >= threshold
|
|
457
|
+
else false
|
|
458
|
+
end
|
|
459
|
+
end
|
|
341
460
|
end
|
|
342
461
|
end
|
|
343
462
|
end
|
data/lib/log_bench/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: log_bench
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Benjamín Silva
|
|
@@ -136,6 +136,7 @@ files:
|
|
|
136
136
|
- lib/log_bench/app/renderer/details.rb
|
|
137
137
|
- lib/log_bench/app/renderer/header.rb
|
|
138
138
|
- lib/log_bench/app/renderer/main.rb
|
|
139
|
+
- lib/log_bench/app/renderer/request_filter_bar.rb
|
|
139
140
|
- lib/log_bench/app/renderer/request_list.rb
|
|
140
141
|
- lib/log_bench/app/renderer/scrollbar.rb
|
|
141
142
|
- lib/log_bench/app/renderer/update_modal.rb
|