rails_live_dashboard 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|