Skip to content

Commit 01e20be

Browse files
authored
Merge pull request #23 from vishaltps/feature/search
feat: add global search functionality
2 parents 27222dd + 1167791 commit 01e20be

11 files changed

Lines changed: 1064 additions & 5 deletions

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
# Git worktrees for parallel development
1717
.worktrees/
18-
1918
# Claude Code personal workflows (local only)
2019
.claude/
2120
CLAUDE.md

app/controllers/solid_queue_monitor/base_controller.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def paginate(relation)
66
PaginationService.new(relation, current_page, per_page).paginate
77
end
88

9-
def render_page(title, content)
9+
def render_page(title, content, search_query: nil)
1010
# Get flash message from instance variable (set by set_flash_message) or session
1111
message = @flash_message
1212
message_type = @flash_type
@@ -27,7 +27,8 @@ def render_page(title, content)
2727
title: title,
2828
content: content,
2929
message: message,
30-
message_type: message_type
30+
message_type: message_type,
31+
search_query: search_query
3132
).generate
3233

3334
render html: html.html_safe
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
module SolidQueueMonitor
4+
class SearchController < BaseController
5+
def index
6+
query = params[:q]
7+
results = SearchService.new(query).search
8+
9+
render_page('Search', SearchResultsPresenter.new(query, results).render, search_query: query)
10+
end
11+
end
12+
end
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# frozen_string_literal: true
2+
3+
module SolidQueueMonitor
4+
class SearchResultsPresenter < BasePresenter
5+
def initialize(query, results)
6+
@query = query
7+
@results = results
8+
end
9+
10+
def render
11+
section_wrapper('Search Results', generate_content)
12+
end
13+
14+
private
15+
16+
def generate_content
17+
if @query.blank?
18+
generate_empty_query_message
19+
elsif total_count.zero?
20+
generate_no_results_message
21+
else
22+
generate_results_summary + generate_all_sections
23+
end
24+
end
25+
26+
def generate_empty_query_message
27+
<<-HTML
28+
<div class="empty-state">
29+
<p>Enter a search term in the header to find jobs across all categories.</p>
30+
</div>
31+
HTML
32+
end
33+
34+
def generate_no_results_message
35+
<<-HTML
36+
<div class="empty-state">
37+
<p>No results found for "#{escape_html(@query)}"</p>
38+
<p class="results-summary">0 results</p>
39+
</div>
40+
HTML
41+
end
42+
43+
def generate_results_summary
44+
<<-HTML
45+
<div class="results-summary">
46+
<p>Found #{total_count} #{total_count == 1 ? 'result' : 'results'} for "#{escape_html(@query)}"</p>
47+
</div>
48+
HTML
49+
end
50+
51+
def generate_all_sections
52+
sections = []
53+
sections << generate_ready_section if @results[:ready].any?
54+
sections << generate_scheduled_section if @results[:scheduled].any?
55+
sections << generate_failed_section if @results[:failed].any?
56+
sections << generate_in_progress_section if @results[:in_progress].any?
57+
sections << generate_completed_section if @results[:completed].any?
58+
sections << generate_recurring_section if @results[:recurring].any?
59+
sections.join
60+
end
61+
62+
def generate_ready_section
63+
generate_section('Ready Jobs', @results[:ready]) do |execution|
64+
generate_job_row(execution.job, execution.queue_name, execution.created_at)
65+
end
66+
end
67+
68+
def generate_scheduled_section
69+
generate_section('Scheduled Jobs', @results[:scheduled]) do |execution|
70+
generate_job_row(execution.job, execution.queue_name, execution.scheduled_at, 'Scheduled for')
71+
end
72+
end
73+
74+
def generate_failed_section
75+
generate_section('Failed Jobs', @results[:failed]) do |execution|
76+
generate_failed_row(execution)
77+
end
78+
end
79+
80+
def generate_in_progress_section
81+
generate_section('In Progress Jobs', @results[:in_progress]) do |execution|
82+
generate_job_row(execution.job, execution.job.queue_name, execution.created_at, 'Started at')
83+
end
84+
end
85+
86+
def generate_completed_section
87+
generate_section('Completed Jobs', @results[:completed]) do |job|
88+
generate_completed_row(job)
89+
end
90+
end
91+
92+
def generate_recurring_section
93+
generate_section('Recurring Tasks', @results[:recurring]) do |task|
94+
generate_recurring_row(task)
95+
end
96+
end
97+
98+
def generate_section(title, items, &block)
99+
<<-HTML
100+
<div class="search-results-section">
101+
<h3>#{title} (#{items.size})</h3>
102+
<div class="table-container">
103+
<table>
104+
<thead>
105+
<tr>
106+
#{section_headers(title)}
107+
</tr>
108+
</thead>
109+
<tbody>
110+
#{items.map(&block).join}
111+
</tbody>
112+
</table>
113+
</div>
114+
</div>
115+
HTML
116+
end
117+
118+
def section_headers(title)
119+
case title
120+
when 'Recurring Tasks'
121+
'<th>Key</th><th>Class</th><th>Schedule</th><th>Queue</th>'
122+
when 'Failed Jobs'
123+
'<th>Job</th><th>Queue</th><th>Error</th><th>Failed At</th>'
124+
when 'Completed Jobs'
125+
'<th>Job</th><th>Queue</th><th>Arguments</th><th>Completed At</th>'
126+
else
127+
'<th>Job</th><th>Queue</th><th>Arguments</th><th>Time</th>'
128+
end
129+
end
130+
131+
def generate_job_row(job, queue_name, time, time_label = 'Created at')
132+
<<-HTML
133+
<tr>
134+
<td><a href="#{job_path(job)}" class="job-class-link">#{job.class_name}</a></td>
135+
<td>#{queue_link(queue_name)}</td>
136+
<td>#{format_arguments(job.arguments)}</td>
137+
<td>
138+
<span class="job-timestamp">#{time_label}: #{format_datetime(time)}</span>
139+
</td>
140+
</tr>
141+
HTML
142+
end
143+
144+
def generate_failed_row(execution)
145+
job = execution.job
146+
<<-HTML
147+
<tr>
148+
<td><a href="#{job_path(job)}" class="job-class-link">#{job.class_name}</a></td>
149+
<td>#{queue_link(job.queue_name)}</td>
150+
<td><div class="error-message">#{escape_html(execution.error.to_s.truncate(100))}</div></td>
151+
<td>
152+
<span class="job-timestamp">#{format_datetime(execution.created_at)}</span>
153+
</td>
154+
</tr>
155+
HTML
156+
end
157+
158+
def generate_completed_row(job)
159+
<<-HTML
160+
<tr>
161+
<td><a href="#{job_path(job)}" class="job-class-link">#{job.class_name}</a></td>
162+
<td>#{queue_link(job.queue_name)}</td>
163+
<td>#{format_arguments(job.arguments)}</td>
164+
<td>
165+
<span class="job-timestamp">#{format_datetime(job.finished_at)}</span>
166+
</td>
167+
</tr>
168+
HTML
169+
end
170+
171+
def generate_recurring_row(task)
172+
<<-HTML
173+
<tr>
174+
<td><strong>#{task.key}</strong></td>
175+
<td>#{task.class_name || '-'}</td>
176+
<td><code>#{task.schedule}</code></td>
177+
<td>#{queue_link(task.queue_name)}</td>
178+
</tr>
179+
HTML
180+
end
181+
182+
def total_count
183+
@total_count ||= @results.values.sum(&:size)
184+
end
185+
186+
def escape_html(text)
187+
text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
188+
end
189+
end
190+
end

app/services/solid_queue_monitor/html_generator.rb

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ class HtmlGenerator
55
include Rails.application.routes.url_helpers
66
include SolidQueueMonitor::Engine.routes.url_helpers
77

8-
def initialize(title:, content:, message: nil, message_type: nil)
8+
def initialize(title:, content:, message: nil, message_type: nil, search_query: nil)
99
@title = title
1010
@content = content
1111
@message = message
1212
@message_type = message_type
13+
@search_query = search_query
1314
end
1415

1516
def generate
@@ -107,7 +108,8 @@ def generate_header
107108
<<-HTML
108109
<header>
109110
<div class="header-top">
110-
<h1>Solid Queue Monitor</h1>
111+
<h1><a href="#{root_path}" class="header-title-link">Solid Queue Monitor</a></h1>
112+
#{generate_search_box}
111113
<div class="header-controls">
112114
#{generate_auto_refresh_controls}
113115
#{generate_theme_toggle}
@@ -128,6 +130,25 @@ def generate_footer
128130
HTML
129131
end
130132

133+
def generate_search_box
134+
search_value = @search_query ? escape_html(@search_query) : ''
135+
<<-HTML
136+
<form method="get" action="#{search_path}" class="header-search-form">
137+
<input type="text" name="q" value="#{search_value}" placeholder="Search by class, queue, job ID, or error..." class="header-search-input">
138+
<button type="submit" class="header-search-button" title="Search">
139+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
140+
<circle cx="11" cy="11" r="8"></circle>
141+
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
142+
</svg>
143+
</button>
144+
</form>
145+
HTML
146+
end
147+
148+
def escape_html(text)
149+
text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
150+
end
151+
131152
def generate_auto_refresh_controls
132153
return '' unless SolidQueueMonitor.auto_refresh_enabled
133154

0 commit comments

Comments
 (0)