rails_live_dashboard 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +63 -0
- data/Rakefile +8 -0
- data/app/assets/builds/rails_live_dashboard/application.css +1 -0
- data/app/assets/builds/rails_live_dashboard/application.js +39 -0
- data/app/assets/builds/rails_live_dashboard/application.js.map +7 -0
- data/app/assets/javascripts/rails_live_dashboard/application.js +2 -0
- data/app/assets/javascripts/rails_live_dashboard/controllers/application.js +9 -0
- data/app/assets/javascripts/rails_live_dashboard/controllers/index.js +6 -0
- data/app/assets/javascripts/rails_live_dashboard/controllers/reveal_controller.js +28 -0
- data/app/assets/javascripts/rails_live_dashboard/controllers/tabs_controller.js +37 -0
- data/app/assets/stylesheets/rails_live_dashboard/application.css +5 -0
- data/app/components/rails_live_dashboard/query_duration_badge_component.html.erb +3 -0
- data/app/components/rails_live_dashboard/query_duration_badge_component.rb +27 -0
- data/app/components/rails_live_dashboard/request_duration_badge_component.html.erb +3 -0
- data/app/components/rails_live_dashboard/request_duration_badge_component.rb +27 -0
- data/app/components/rails_live_dashboard/request_method_badge_component.html.erb +6 -0
- data/app/components/rails_live_dashboard/request_method_badge_component.rb +24 -0
- data/app/components/rails_live_dashboard/request_status_badge_component.html.erb +3 -0
- data/app/components/rails_live_dashboard/request_status_badge_component.rb +22 -0
- data/app/controllers/rails_live_dashboard/application_controller.rb +4 -0
- data/app/controllers/rails_live_dashboard/clean_controller.rb +9 -0
- data/app/controllers/rails_live_dashboard/dashboard_controller.rb +8 -0
- data/app/controllers/rails_live_dashboard/exceptions_controller.rb +13 -0
- data/app/controllers/rails_live_dashboard/queries_controller.rb +11 -0
- data/app/controllers/rails_live_dashboard/requests_controller.rb +13 -0
- data/app/controllers/rails_live_dashboard/widgets/slowest_queries_controller.rb +11 -0
- data/app/controllers/rails_live_dashboard/widgets/slowest_requests_controller.rb +11 -0
- data/app/helpers/rails_live_dashboard/application_helper.rb +4 -0
- data/app/helpers/rails_live_dashboard/clean_helper.rb +4 -0
- data/app/helpers/rails_live_dashboard/dashboard_helper.rb +4 -0
- data/app/helpers/rails_live_dashboard/exceptions_helper.rb +4 -0
- data/app/helpers/rails_live_dashboard/requests_helper.rb +4 -0
- data/app/jobs/rails_live_dashboard/application_job.rb +4 -0
- data/app/mailers/rails_live_dashboard/application_mailer.rb +6 -0
- data/app/models/rails_live_dashboard/application_record.rb +5 -0
- data/app/models/rails_live_dashboard/entry.rb +5 -0
- data/app/models/rails_live_dashboard/exception.rb +31 -0
- data/app/models/rails_live_dashboard/query.rb +5 -0
- data/app/models/rails_live_dashboard/request.rb +45 -0
- data/app/models/rails_live_dashboard/types/content.rb +32 -0
- data/app/models/rails_live_dashboard/types/exception_content.rb +42 -0
- data/app/models/rails_live_dashboard/types/query_content.rb +42 -0
- data/app/models/rails_live_dashboard/types/request_content.rb +49 -0
- data/app/views/layouts/rails_live_dashboard/application.html.erb +18 -0
- data/app/views/rails_live_dashboard/dashboard/show.html.erb +27 -0
- data/app/views/rails_live_dashboard/exceptions/_tabs.html.erb +16 -0
- data/app/views/rails_live_dashboard/exceptions/index.html.erb +55 -0
- data/app/views/rails_live_dashboard/exceptions/show.html.erb +41 -0
- data/app/views/rails_live_dashboard/queries/_list.html.erb +43 -0
- data/app/views/rails_live_dashboard/queries/index.html.erb +11 -0
- data/app/views/rails_live_dashboard/queries/show.html.erb +33 -0
- data/app/views/rails_live_dashboard/requests/_contents.html.erb +26 -0
- data/app/views/rails_live_dashboard/requests/_exceptions.html.erb +43 -0
- data/app/views/rails_live_dashboard/requests/_queries.html.erb +11 -0
- data/app/views/rails_live_dashboard/requests/_relateds.html.erb +23 -0
- data/app/views/rails_live_dashboard/requests/index.html.erb +48 -0
- data/app/views/rails_live_dashboard/requests/show.html.erb +46 -0
- data/app/views/rails_live_dashboard/shared/_header.html.erb +43 -0
- data/app/views/rails_live_dashboard/widgets/slowest_queries/show.html.erb +42 -0
- data/app/views/rails_live_dashboard/widgets/slowest_requests/show.html.erb +42 -0
- data/config/routes.rb +14 -0
- data/db/migrate/20240213133517_create_rails_live_dashboard_entries.rb +14 -0
- data/lib/generators/rails_live_dashboard/install_generator.rb +11 -0
- data/lib/generators/templates/initializer.rb +3 -0
- data/lib/rails_live_dashboard/configuration.rb +13 -0
- data/lib/rails_live_dashboard/context.rb +19 -0
- data/lib/rails_live_dashboard/engine.rb +28 -0
- data/lib/rails_live_dashboard/recorders/exception_recorder.rb +42 -0
- data/lib/rails_live_dashboard/recorders/query_recorder.rb +30 -0
- data/lib/rails_live_dashboard/recorders/request_recorder.rb +30 -0
- data/lib/rails_live_dashboard/subscribers/action_controller_subscriber.rb +28 -0
- data/lib/rails_live_dashboard/subscribers/active_record_subscriber.rb +23 -0
- data/lib/rails_live_dashboard/version.rb +3 -0
- data/lib/rails_live_dashboard.rb +19 -0
- data/lib/tasks/rails_live_dashboard_tasks.rake +4 -0
- metadata +136 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["item"];
|
5
|
+
static classes = ["hidden"];
|
6
|
+
|
7
|
+
connect() {
|
8
|
+
this.class = this.hasHiddenClass ? this.hiddenClass : "hidden";
|
9
|
+
}
|
10
|
+
|
11
|
+
toggle() {
|
12
|
+
this.itemTargets.forEach((item) => {
|
13
|
+
item.classList.toggle(this.class);
|
14
|
+
});
|
15
|
+
}
|
16
|
+
|
17
|
+
show() {
|
18
|
+
this.itemTargets.forEach((item) => {
|
19
|
+
item.classList.remove(this.class);
|
20
|
+
});
|
21
|
+
}
|
22
|
+
|
23
|
+
hide() {
|
24
|
+
this.itemTargets.forEach((item) => {
|
25
|
+
item.classList.add(this.class);
|
26
|
+
});
|
27
|
+
}
|
28
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static classes = ["active"];
|
5
|
+
static targets = ["button", "tab"];
|
6
|
+
static values = { defaultTab: String };
|
7
|
+
|
8
|
+
connect() {
|
9
|
+
this.tabTargets.map((tab) => {
|
10
|
+
if (tab.id != this.defaultTabValue) {
|
11
|
+
tab.hidden = true;
|
12
|
+
}
|
13
|
+
});
|
14
|
+
|
15
|
+
this.buttonTargets.find((button) => {
|
16
|
+
if (button.id === this.defaultTabValue) {
|
17
|
+
button.classList.add(...this.activeClasses);
|
18
|
+
}
|
19
|
+
});
|
20
|
+
}
|
21
|
+
|
22
|
+
select(event) {
|
23
|
+
let selectedTab = this.tabTargets.find(
|
24
|
+
(element) => element.id === event.currentTarget.id
|
25
|
+
);
|
26
|
+
|
27
|
+
if (selectedTab.hidden) {
|
28
|
+
this.tabTargets.map((tab) => (tab.hidden = true));
|
29
|
+
this.buttonTargets.map((button) =>
|
30
|
+
button.classList.remove(...this.activeClasses)
|
31
|
+
);
|
32
|
+
|
33
|
+
selectedTab.hidden = false;
|
34
|
+
event.currentTarget.classList.add(...this.activeClasses);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLiveDashboard
|
4
|
+
class QueryDurationBadgeComponent < ViewComponent::Base
|
5
|
+
CLASSES = {
|
6
|
+
ok: 'bg-green-50 text-green-700 ring-green-600/20',
|
7
|
+
warning: 'bg-orange-50 text-orange-700 ring-orange-600/10',
|
8
|
+
danger: 'bg-red-50 text-red-700 ring-red-600/10'
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
def initialize(duration)
|
12
|
+
super
|
13
|
+
|
14
|
+
@duration = duration
|
15
|
+
@classes = duration_classes
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def duration_classes
|
21
|
+
return CLASSES[:ok] if @duration < 10
|
22
|
+
return CLASSES[:warning] if @duration < 100
|
23
|
+
|
24
|
+
CLASSES[:danger]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLiveDashboard
|
4
|
+
class RequestDurationBadgeComponent < ViewComponent::Base
|
5
|
+
CLASSES = {
|
6
|
+
ok: 'bg-green-50 text-green-700 ring-green-600/20',
|
7
|
+
warning: 'bg-orange-50 text-orange-700 ring-orange-600/10',
|
8
|
+
danger: 'bg-red-50 text-red-700 ring-red-600/10'
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
def initialize(duration)
|
12
|
+
super
|
13
|
+
|
14
|
+
@duration = duration
|
15
|
+
@classes = duration_classes
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def duration_classes
|
21
|
+
return CLASSES[:ok] if @duration < 100
|
22
|
+
return CLASSES[:warning] if @duration < 500
|
23
|
+
|
24
|
+
CLASSES[:danger]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<span class="inline-flex items-center gap-x-1.5 rounded-md px-2 py-1 text-xs font-medium text-gray-900 ring-1 ring-inset ring-gray-200">
|
2
|
+
<svg class="<%= class_names('h-1.5 w-1.5', @classes) %>" viewBox="0 0 6 6" aria-hidden="true">
|
3
|
+
<circle cx="3" cy="3" r="3" />
|
4
|
+
</svg>
|
5
|
+
<%= @method %>
|
6
|
+
</span>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLiveDashboard
|
4
|
+
class RequestMethodBadgeComponent < ViewComponent::Base
|
5
|
+
CLASSES = {
|
6
|
+
get: 'fill-green-500',
|
7
|
+
post: 'fill-yellow-500',
|
8
|
+
put: 'fill-blue-500',
|
9
|
+
patch: 'fill-indigo-500',
|
10
|
+
delete: 'fill-red-500',
|
11
|
+
head: 'fill-green-500',
|
12
|
+
option: 'fill-purple-500'
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def initialize(method, turbo_stream: false)
|
16
|
+
super
|
17
|
+
|
18
|
+
return if method.nil?
|
19
|
+
|
20
|
+
@classes = CLASSES[method.downcase.to_sym]
|
21
|
+
@method = "#{method} #{'- Turbo Stream' if turbo_stream}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLiveDashboard
|
4
|
+
class RequestStatusBadgeComponent < ViewComponent::Base
|
5
|
+
CLASSES = {
|
6
|
+
100..199 => 'bg-blue-50 text-blue-700 ring-blue-700/10',
|
7
|
+
200..299 => 'bg-green-50 text-green-700 ring-green-600/20',
|
8
|
+
300..399 => 'bg-yellow-50 text-yellow-800 ring-yellow-600/20',
|
9
|
+
400..499 => 'bg-orange-50 text-orange-700 ring-orange-600/10',
|
10
|
+
500..599 => 'bg-red-50 text-red-700 ring-red-600/10'
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
def initialize(status)
|
14
|
+
super
|
15
|
+
|
16
|
+
return if status.nil?
|
17
|
+
|
18
|
+
@status = status
|
19
|
+
@classes = CLASSES.detect { |k, _v| k === status }&.last
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module RailsLiveDashboard
|
2
|
+
class ExceptionsController < ApplicationController
|
3
|
+
def index
|
4
|
+
@exceptions = Exception
|
5
|
+
.where(should_show: true)
|
6
|
+
.order(created_at: :desc)
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
@exception = Exception.find(params[:id])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module RailsLiveDashboard
|
2
|
+
class RequestsController < ApplicationController
|
3
|
+
def index
|
4
|
+
@requests = Request.order(created_at: :desc)
|
5
|
+
end
|
6
|
+
|
7
|
+
def show
|
8
|
+
@request = Request.find(params[:id])
|
9
|
+
@exceptions = Exception.where(batch_id: @request.batch_id)
|
10
|
+
@queries = Query.where(batch_id: @request.batch_id)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module RailsLiveDashboard
|
2
|
+
class Exception < Entry
|
3
|
+
attribute :content, Types::ExceptionContent.new
|
4
|
+
|
5
|
+
scope :of_class, ->(exception_class) { where("content->>'class' = ?", exception_class) }
|
6
|
+
|
7
|
+
def class_name
|
8
|
+
content.class
|
9
|
+
end
|
10
|
+
|
11
|
+
def message
|
12
|
+
content.message
|
13
|
+
end
|
14
|
+
|
15
|
+
def occurrences
|
16
|
+
content.occurrences
|
17
|
+
end
|
18
|
+
|
19
|
+
def file
|
20
|
+
content.file
|
21
|
+
end
|
22
|
+
|
23
|
+
def line
|
24
|
+
content.line
|
25
|
+
end
|
26
|
+
|
27
|
+
def backtrace
|
28
|
+
content.backtrace
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module RailsLiveDashboard
|
2
|
+
class Request < Entry
|
3
|
+
attribute :content, Types::RequestContent.new
|
4
|
+
|
5
|
+
def method
|
6
|
+
content.method
|
7
|
+
end
|
8
|
+
|
9
|
+
def path
|
10
|
+
content.path
|
11
|
+
end
|
12
|
+
|
13
|
+
def status_code
|
14
|
+
content.status
|
15
|
+
end
|
16
|
+
|
17
|
+
def controller
|
18
|
+
content.controller
|
19
|
+
end
|
20
|
+
|
21
|
+
def controller_action
|
22
|
+
content.action
|
23
|
+
end
|
24
|
+
|
25
|
+
def duration
|
26
|
+
content.duration.round
|
27
|
+
end
|
28
|
+
|
29
|
+
def payload
|
30
|
+
content.params
|
31
|
+
end
|
32
|
+
|
33
|
+
def headers
|
34
|
+
content.headers
|
35
|
+
end
|
36
|
+
|
37
|
+
def body
|
38
|
+
content.body
|
39
|
+
end
|
40
|
+
|
41
|
+
def turbo_stream?
|
42
|
+
content.format == 'turbo_stream'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module RailsLiveDashboard
|
2
|
+
module Types
|
3
|
+
class Content < ActiveRecord::Type::Value
|
4
|
+
CONTENT_STRUCT = Struct.new(*self.PAYLOAD)
|
5
|
+
|
6
|
+
def type
|
7
|
+
:jsonb
|
8
|
+
end
|
9
|
+
|
10
|
+
def cast(value)
|
11
|
+
sanitized = sanitize_input(value)
|
12
|
+
CONTENT_STRUCT.new(**sanitized)
|
13
|
+
end
|
14
|
+
|
15
|
+
def deserialize(value)
|
16
|
+
decoded = ActiveSupport::JSON.decode(value)
|
17
|
+
sanitized = sanitize_input(decoded)
|
18
|
+
CONTENT_STRUCT.new(**sanitized)
|
19
|
+
end
|
20
|
+
|
21
|
+
def serialize(value)
|
22
|
+
ActiveSupport::JSON.encode(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def sanitize_input(input)
|
28
|
+
input.slice(*self.PAYLOAD)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module RailsLiveDashboard
|
2
|
+
module Types
|
3
|
+
class ExceptionContent < ActiveRecord::Type::Value
|
4
|
+
PAYLOAD =
|
5
|
+
%i[
|
6
|
+
class
|
7
|
+
message
|
8
|
+
file
|
9
|
+
line
|
10
|
+
backtrace
|
11
|
+
occurrences
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
CONTENT_STRUCT = Struct.new(*PAYLOAD)
|
15
|
+
|
16
|
+
def type
|
17
|
+
:jsonb
|
18
|
+
end
|
19
|
+
|
20
|
+
def cast(value)
|
21
|
+
sanitized = sanitize_input(value)
|
22
|
+
CONTENT_STRUCT.new(**sanitized)
|
23
|
+
end
|
24
|
+
|
25
|
+
def deserialize(value)
|
26
|
+
decoded = ActiveSupport::JSON.decode(value)&.symbolize_keys
|
27
|
+
sanitized = sanitize_input(decoded)
|
28
|
+
CONTENT_STRUCT.new(**sanitized)
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialize(value)
|
32
|
+
ActiveSupport::JSON.encode(value)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def sanitize_input(input)
|
38
|
+
input.slice(*PAYLOAD)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module RailsLiveDashboard
|
2
|
+
module Types
|
3
|
+
class QueryContent < ActiveRecord::Type::Value
|
4
|
+
PAYLOAD =
|
5
|
+
%i[
|
6
|
+
name
|
7
|
+
sql
|
8
|
+
parameters
|
9
|
+
duration
|
10
|
+
kind
|
11
|
+
cached
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
CONTENT_STRUCT = Struct.new(*PAYLOAD)
|
15
|
+
|
16
|
+
def type
|
17
|
+
:jsonb
|
18
|
+
end
|
19
|
+
|
20
|
+
def cast(value)
|
21
|
+
sanitized = sanitize_input(value)
|
22
|
+
CONTENT_STRUCT.new(**sanitized)
|
23
|
+
end
|
24
|
+
|
25
|
+
def deserialize(value)
|
26
|
+
decoded = ActiveSupport::JSON.decode(value)&.symbolize_keys
|
27
|
+
sanitized = sanitize_input(decoded)
|
28
|
+
CONTENT_STRUCT.new(**sanitized)
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialize(value)
|
32
|
+
ActiveSupport::JSON.encode(value)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def sanitize_input(input)
|
38
|
+
input.slice(*PAYLOAD)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module RailsLiveDashboard
|
2
|
+
module Types
|
3
|
+
class RequestContent < ActiveRecord::Type::Value
|
4
|
+
PAYLOAD =
|
5
|
+
%i[
|
6
|
+
controller
|
7
|
+
action
|
8
|
+
params
|
9
|
+
format
|
10
|
+
method
|
11
|
+
path
|
12
|
+
status
|
13
|
+
headers
|
14
|
+
body
|
15
|
+
duration
|
16
|
+
view_duration
|
17
|
+
db_duration
|
18
|
+
allocations
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
CONTENT_STRUCT = Struct.new(*PAYLOAD)
|
22
|
+
|
23
|
+
def type
|
24
|
+
:jsonb
|
25
|
+
end
|
26
|
+
|
27
|
+
def cast(value)
|
28
|
+
sanitized = sanitize_input(value)
|
29
|
+
CONTENT_STRUCT.new(**sanitized)
|
30
|
+
end
|
31
|
+
|
32
|
+
def deserialize(value)
|
33
|
+
decoded = ActiveSupport::JSON.decode(value)&.symbolize_keys
|
34
|
+
sanitized = sanitize_input(decoded)
|
35
|
+
CONTENT_STRUCT.new(**sanitized)
|
36
|
+
end
|
37
|
+
|
38
|
+
def serialize(value)
|
39
|
+
ActiveSupport::JSON.encode(value)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def sanitize_input(input)
|
45
|
+
input.slice(*PAYLOAD)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Rails Live Dashboard</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag "rails_live_dashboard/application", data_turbo_track: 'reload', media: "all" %>
|
9
|
+
<%= javascript_include_tag "rails_live_dashboard/application", data_turbo_track: 'reload', defer: true %>
|
10
|
+
</head>
|
11
|
+
<body class="bg-gray-200 pb-10">
|
12
|
+
<%= render "rails_live_dashboard/shared/header" %>
|
13
|
+
|
14
|
+
<main class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
15
|
+
<%= yield %>
|
16
|
+
</main>
|
17
|
+
</body>
|
18
|
+
</html>
|