query_owl 0.6.0 → 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/app/assets/stylesheets/query_owl/_02_layout.css +2 -1
- data/app/assets/stylesheets/query_owl/_03_table.css +10 -1
- data/app/assets/stylesheets/query_owl/_05_filters.css +80 -0
- data/app/controllers/query_owl/slow_queries_controller.rb +27 -7
- data/app/helpers/query_owl/application_helper.rb +16 -0
- data/app/javascript/query_owl/application.js +6 -0
- data/app/javascript/query_owl/table_filter_controller.js +32 -0
- data/app/views/layouts/query_owl/application.html.erb +1 -0
- data/app/views/query_owl/slow_queries/index.html.erb +75 -37
- data/config/importmap.rb +4 -0
- data/lib/query_owl/engine.rb +16 -0
- data/lib/query_owl/version.rb +1 -1
- metadata +33 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: daca52db3c3f2564dc0e0e51b2271e995db9042ffd82d2b2a2ef25529fc5cbd4
|
|
4
|
+
data.tar.gz: 47cad0afa15471a0fb60dc6366dbadcc5bc480b2cf7b17321198230fd46f8d8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82cb7e1f4f42c553731c3e0ba32f4b4f7f01a0bf21c837644371fcb02313d4995f27f84fd70f574df3f6ef889266ddec6ba2c7bbf3c5302f3ee0c1084d431539
|
|
7
|
+
data.tar.gz: 8bf2d7078c201e0bb673943194dbb3d98d07e30a1ce6a9c64ac6c72f0416870aca1349a21050779c7edbc92c1d5f2609219d44fed7745591d60ca83dfe98c387
|
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
.qo-header h1 { font-size: 1.4rem; font-weight: 600; }
|
|
3
3
|
.qo-header p { color: var(--muted); margin-top: 0.2rem; }
|
|
4
4
|
|
|
5
|
-
.qo-empty
|
|
5
|
+
.qo-empty { color: var(--muted); font-style: italic; margin-top: 1rem; }
|
|
6
|
+
.qo-summary { margin-bottom: 0.5rem; }
|
|
@@ -31,4 +31,13 @@
|
|
|
31
31
|
.qo-table td:nth-child(2) { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
32
32
|
|
|
33
33
|
.qo-request { white-space: nowrap; }
|
|
34
|
-
.qo-path { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 11px; opacity: .7; }
|
|
34
|
+
.qo-path { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 11px; opacity: .7; }
|
|
35
|
+
|
|
36
|
+
.qo-sort-link {
|
|
37
|
+
color: inherit;
|
|
38
|
+
text-decoration: none;
|
|
39
|
+
display: block;
|
|
40
|
+
}
|
|
41
|
+
.qo-sort-link:hover { color: var(--accent, #4f46e5); }
|
|
42
|
+
|
|
43
|
+
.qo-sort-active { color: var(--accent, #4f46e5); }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
.qo-filters {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 0.75rem;
|
|
5
|
+
margin-bottom: 1rem;
|
|
6
|
+
padding: 0.6rem 1rem;
|
|
7
|
+
background: var(--surface);
|
|
8
|
+
border-radius: var(--radius);
|
|
9
|
+
box-shadow: var(--shadow);
|
|
10
|
+
flex-wrap: wrap;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.qo-filters label {
|
|
14
|
+
font-size: 11px;
|
|
15
|
+
font-weight: 600;
|
|
16
|
+
text-transform: uppercase;
|
|
17
|
+
letter-spacing: .04em;
|
|
18
|
+
color: var(--muted);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.qo-select,
|
|
22
|
+
.qo-input {
|
|
23
|
+
font-family: inherit;
|
|
24
|
+
font-size: 13px;
|
|
25
|
+
padding: 0.3rem 0.6rem;
|
|
26
|
+
border: 1px solid var(--border);
|
|
27
|
+
border-radius: var(--radius);
|
|
28
|
+
background: var(--bg);
|
|
29
|
+
color: var(--text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.qo-input { min-width: 160px; }
|
|
33
|
+
|
|
34
|
+
.qo-input-wrapper {
|
|
35
|
+
position: relative;
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.qo-input-wrapper .qo-input { padding-right: 1.6rem; }
|
|
41
|
+
|
|
42
|
+
.qo-input-clear {
|
|
43
|
+
position: absolute;
|
|
44
|
+
right: 0.4rem;
|
|
45
|
+
background: none;
|
|
46
|
+
border: none;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
color: var(--muted);
|
|
49
|
+
font-size: 11px;
|
|
50
|
+
line-height: 1;
|
|
51
|
+
padding: 0.15rem 0.2rem;
|
|
52
|
+
border-radius: 2px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.qo-input-clear:hover { color: var(--text); background: var(--border); }
|
|
56
|
+
|
|
57
|
+
.qo-select:focus,
|
|
58
|
+
.qo-input:focus {
|
|
59
|
+
outline: none;
|
|
60
|
+
border-color: #86b7fe;
|
|
61
|
+
box-shadow: 0 0 0 2px rgba(13,110,253,.15);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.qo-btn {
|
|
65
|
+
display: inline-block;
|
|
66
|
+
padding: 0.3rem 0.75rem;
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
border-radius: var(--radius);
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
font-family: inherit;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.qo-btn--muted {
|
|
75
|
+
background: transparent;
|
|
76
|
+
color: var(--muted);
|
|
77
|
+
border: 1px solid var(--border);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.qo-btn--muted:hover { background: var(--bg); color: var(--text); }
|
|
@@ -6,23 +6,43 @@ module QueryOwl
|
|
|
6
6
|
|
|
7
7
|
before_action :check_dashboard_enabled, if: -> { request.format.html? }
|
|
8
8
|
|
|
9
|
+
SORTABLE_COLUMNS = %w[type info recorded_at].freeze
|
|
10
|
+
|
|
9
11
|
def index
|
|
10
12
|
filters = request.query_parameters
|
|
11
13
|
events = EventStore.all
|
|
12
|
-
events = events.select { |e| e[:type].to_s == filters["type"] }
|
|
13
|
-
events = events.select { |e| e[:controller]
|
|
14
|
-
events = events.select { |e| e[:action] == filters["action"] }
|
|
14
|
+
events = events.select { |e| e[:type].to_s == filters["type"] } if filters["type"].present?
|
|
15
|
+
events = events.select { |e| e[:controller].to_s.include?(filters["controller"]) } if filters["controller"].present?
|
|
16
|
+
events = events.select { |e| e[:action] == filters["action"] } if filters["action"].present?
|
|
15
17
|
|
|
16
18
|
respond_to do |format|
|
|
17
19
|
format.json { render json: events }
|
|
18
|
-
format.html
|
|
20
|
+
format.html do
|
|
21
|
+
@type_filter = filters["type"].presence
|
|
22
|
+
@controller_filter = filters["controller"].presence
|
|
23
|
+
@sort = SORTABLE_COLUMNS.include?(filters["sort"]) ? filters["sort"] : "recorded_at"
|
|
24
|
+
@direction = filters["direction"] == "asc" ? "asc" : "desc"
|
|
25
|
+
@events = sorted_events(events, @sort, @direction)
|
|
26
|
+
end
|
|
19
27
|
end
|
|
20
28
|
end
|
|
21
29
|
|
|
22
30
|
private
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
def sorted_events(events, sort, direction)
|
|
33
|
+
sorted = case sort
|
|
34
|
+
when "type"
|
|
35
|
+
events.sort_by { |e| e[:type].to_s }
|
|
36
|
+
when "info"
|
|
37
|
+
events.sort_by { |e| e[:duration_ms] || e[:count] || 0 }
|
|
38
|
+
else
|
|
39
|
+
events.sort_by { |e| e[:recorded_at] || Time.at(0) }
|
|
40
|
+
end
|
|
41
|
+
direction == "asc" ? sorted : sorted.reverse
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def check_dashboard_enabled
|
|
45
|
+
head :forbidden unless QueryOwl.config.dashboard_enabled
|
|
46
|
+
end
|
|
27
47
|
end
|
|
28
48
|
end
|
|
@@ -5,5 +5,21 @@ module QueryOwl
|
|
|
5
5
|
css = dir.glob("_*.css").sort.map(&:read).join("\n")
|
|
6
6
|
content_tag(:style, css.html_safe)
|
|
7
7
|
end
|
|
8
|
+
|
|
9
|
+
def sort_th(label, column, current_sort, current_dir)
|
|
10
|
+
active = current_sort == column.to_s
|
|
11
|
+
next_dir = (active && current_dir == "asc") ? "desc" : "asc"
|
|
12
|
+
indicator = active ? (current_dir == "asc" ? " ▲" : " ▼") : ""
|
|
13
|
+
params = request.query_parameters.merge("sort" => column, "direction" => next_dir)
|
|
14
|
+
href = "?" + params.to_query
|
|
15
|
+
content_tag(:th) do
|
|
16
|
+
link_to(
|
|
17
|
+
"#{label}#{indicator}".html_safe,
|
|
18
|
+
href,
|
|
19
|
+
class: ["qo-sort-link", ("qo-sort-active" if active)].compact.join(" "),
|
|
20
|
+
data: { turbo_frame: "qo-events" }
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
8
24
|
end
|
|
9
25
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["controllerInput", "clearButton"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this._updateClear()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
filter({ target }) {
|
|
11
|
+
clearTimeout(this._timer)
|
|
12
|
+
this._updateClear()
|
|
13
|
+
this._timer = setTimeout(() => target.form.requestSubmit(), 300)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
select({ target }) {
|
|
17
|
+
clearTimeout(this._timer)
|
|
18
|
+
target.form.requestSubmit()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
clearController() {
|
|
22
|
+
this.controllerInputTarget.value = ""
|
|
23
|
+
this._updateClear()
|
|
24
|
+
this.controllerInputTarget.form.requestSubmit()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_updateClear() {
|
|
28
|
+
if (this.hasClearButtonTarget) {
|
|
29
|
+
this.clearButtonTarget.hidden = this.controllerInputTarget.value.length === 0
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,43 +1,81 @@
|
|
|
1
1
|
<div class="qo-header">
|
|
2
2
|
<h1>QueryOwl</h1>
|
|
3
|
-
<p>Last <%= @events.length %> detected event<%= "s" unless @events.length == 1 %> (newest first)</p>
|
|
4
3
|
</div>
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
<form class="qo-filters" action="<%= slow_queries_path %>" method="get"
|
|
6
|
+
data-controller="table-filter"
|
|
7
|
+
data-turbo-frame="qo-events">
|
|
8
|
+
<input type="hidden" name="sort" value="<%= @sort %>">
|
|
9
|
+
<input type="hidden" name="direction" value="<%= @direction %>">
|
|
10
|
+
|
|
11
|
+
<label for="qo-type-filter">Type</label>
|
|
12
|
+
<select id="qo-type-filter" name="type" class="qo-select"
|
|
13
|
+
data-action="change->table-filter#select">
|
|
14
|
+
<option value="">All</option>
|
|
15
|
+
<option value="n_plus_one" <%= "selected" if @type_filter == "n_plus_one" %>>N+1</option>
|
|
16
|
+
<option value="slow_query" <%= "selected" if @type_filter == "slow_query" %>>Slow Query</option>
|
|
17
|
+
<option value="unused_eager_load" <%= "selected" if @type_filter == "unused_eager_load" %>>Unused Eager Load</option>
|
|
18
|
+
</select>
|
|
19
|
+
|
|
20
|
+
<label for="qo-controller-filter">Controller</label>
|
|
21
|
+
<div class="qo-input-wrapper">
|
|
22
|
+
<input id="qo-controller-filter" type="text" name="controller" class="qo-input"
|
|
23
|
+
placeholder="e.g. widgets"
|
|
24
|
+
value="<%= @controller_filter %>"
|
|
25
|
+
data-table-filter-target="controllerInput"
|
|
26
|
+
data-action="input->table-filter#filter">
|
|
27
|
+
<button type="button" class="qo-input-clear" aria-label="Clear controller filter"
|
|
28
|
+
data-table-filter-target="clearButton"
|
|
29
|
+
data-action="click->table-filter#clearController"
|
|
30
|
+
<%= "hidden" if @controller_filter.blank? %>>✕</button>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<% if @type_filter.present? || @controller_filter.present? %>
|
|
34
|
+
<%= link_to "Clear", slow_queries_path, class: "qo-btn qo-btn--muted" %>
|
|
35
|
+
<% end %>
|
|
36
|
+
</form>
|
|
37
|
+
|
|
38
|
+
<turbo-frame id="qo-events">
|
|
39
|
+
<p class="qo-summary qo-muted">
|
|
40
|
+
<%= @events.length %> event<%= "s" unless @events.length == 1 %><%= " (filtered)" if @type_filter.present? || @controller_filter.present? %>
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<% if @events.empty? %>
|
|
44
|
+
<p class="qo-empty">No events detected yet.</p>
|
|
45
|
+
<% else %>
|
|
46
|
+
<table class="qo-table">
|
|
47
|
+
<thead>
|
|
22
48
|
<tr>
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
</
|
|
29
|
-
<td class="qo-request qo-muted">
|
|
30
|
-
<% if event[:controller] || event[:action] %>
|
|
31
|
-
<span class="qo-monospace"><%= [event[:controller], event[:action]].compact.join("#") %></span>
|
|
32
|
-
<% end %>
|
|
33
|
-
<% if event[:path] %>
|
|
34
|
-
<br><span class="qo-path"><%= event[:path] %></span>
|
|
35
|
-
<% end %>
|
|
36
|
-
</td>
|
|
37
|
-
<td class="qo-muted"><%= event[:recorded_at]&.strftime("%H:%M:%S") %></td>
|
|
38
|
-
<td class="qo-monospace qo-muted"><%= Array(event[:backtrace]).first %></td>
|
|
49
|
+
<%= sort_th("Type", "type", @sort, @direction) %>
|
|
50
|
+
<th>SQL / Details</th>
|
|
51
|
+
<%= sort_th("Info", "info", @sort, @direction) %>
|
|
52
|
+
<th>Request</th>
|
|
53
|
+
<%= sort_th("Recorded At", "recorded_at", @sort, @direction) %>
|
|
54
|
+
<th>Backtrace</th>
|
|
39
55
|
</tr>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
</thead>
|
|
57
|
+
<tbody>
|
|
58
|
+
<% @events.each do |event| %>
|
|
59
|
+
<tr>
|
|
60
|
+
<td><span class="qo-badge qo-badge--<%= event[:type] %>"><%= event[:type].to_s.tr("_", " ") %></span></td>
|
|
61
|
+
<td class="qo-monospace"><%= event[:sql] || "#{event[:model]}##{event[:association]}" %></td>
|
|
62
|
+
<td class="qo-muted">
|
|
63
|
+
<% if event[:count] %>count: <%= event[:count] %><% end %>
|
|
64
|
+
<% if event[:duration_ms] %><%= event[:duration_ms] %>ms<% end %>
|
|
65
|
+
</td>
|
|
66
|
+
<td class="qo-request qo-muted">
|
|
67
|
+
<% if event[:controller] || event[:action] %>
|
|
68
|
+
<span class="qo-monospace"><%= [event[:controller], event[:action]].compact.join("#") %></span>
|
|
69
|
+
<% end %>
|
|
70
|
+
<% if event[:path] %>
|
|
71
|
+
<br><span class="qo-path"><%= event[:path] %></span>
|
|
72
|
+
<% end %>
|
|
73
|
+
</td>
|
|
74
|
+
<td class="qo-muted"><%= event[:recorded_at]&.strftime("%H:%M:%S") %></td>
|
|
75
|
+
<td class="qo-monospace qo-muted"><%= Array(event[:backtrace]).first %></td>
|
|
76
|
+
</tr>
|
|
77
|
+
<% end %>
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
<% end %>
|
|
81
|
+
</turbo-frame>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"
|
|
2
|
+
pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
|
|
3
|
+
pin "query_owl", to: "query_owl/application.js"
|
|
4
|
+
pin "query_owl/table_filter_controller", to: "query_owl/table_filter_controller.js"
|
data/lib/query_owl/engine.rb
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
require "turbo-rails"
|
|
2
|
+
require "importmap-rails"
|
|
3
|
+
|
|
1
4
|
module QueryOwl
|
|
2
5
|
class Engine < ::Rails::Engine
|
|
3
6
|
isolate_namespace QueryOwl
|
|
4
7
|
config.generators.api_only = true
|
|
5
8
|
|
|
9
|
+
initializer "query_owl.assets" do |app|
|
|
10
|
+
if app.config.respond_to?(:assets)
|
|
11
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "query_owl.importmap", before: "importmap" do |app|
|
|
16
|
+
if app.config.respond_to?(:importmap)
|
|
17
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
18
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
6
22
|
initializer "query_owl.subscribe" do
|
|
7
23
|
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
8
24
|
next unless QueryOwl.config.enabled
|
data/lib/query_owl/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: query_owl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -23,6 +23,34 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: importmap-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: turbo-rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
26
54
|
description: A leaner alternative to Bullet. Detects N+1 queries and slow queries
|
|
27
55
|
in development, logging structured warnings to your Rails logger without the noise.
|
|
28
56
|
email:
|
|
@@ -38,14 +66,18 @@ files:
|
|
|
38
66
|
- app/assets/stylesheets/query_owl/_02_layout.css
|
|
39
67
|
- app/assets/stylesheets/query_owl/_03_table.css
|
|
40
68
|
- app/assets/stylesheets/query_owl/_04_badges.css
|
|
69
|
+
- app/assets/stylesheets/query_owl/_05_filters.css
|
|
41
70
|
- app/controllers/query_owl/application_controller.rb
|
|
42
71
|
- app/controllers/query_owl/slow_queries_controller.rb
|
|
43
72
|
- app/helpers/query_owl/application_helper.rb
|
|
73
|
+
- app/javascript/query_owl/application.js
|
|
74
|
+
- app/javascript/query_owl/table_filter_controller.js
|
|
44
75
|
- app/jobs/query_owl/application_job.rb
|
|
45
76
|
- app/mailers/query_owl/application_mailer.rb
|
|
46
77
|
- app/models/query_owl/application_record.rb
|
|
47
78
|
- app/views/layouts/query_owl/application.html.erb
|
|
48
79
|
- app/views/query_owl/slow_queries/index.html.erb
|
|
80
|
+
- config/importmap.rb
|
|
49
81
|
- config/routes.rb
|
|
50
82
|
- lib/generators/query_owl/install/install_generator.rb
|
|
51
83
|
- lib/generators/query_owl/install/templates/initializer.rb
|