rails_live_dashboard 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +63 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/rails_live_dashboard/application.css +1 -0
  6. data/app/assets/builds/rails_live_dashboard/application.js +39 -0
  7. data/app/assets/builds/rails_live_dashboard/application.js.map +7 -0
  8. data/app/assets/javascripts/rails_live_dashboard/application.js +2 -0
  9. data/app/assets/javascripts/rails_live_dashboard/controllers/application.js +9 -0
  10. data/app/assets/javascripts/rails_live_dashboard/controllers/index.js +6 -0
  11. data/app/assets/javascripts/rails_live_dashboard/controllers/reveal_controller.js +28 -0
  12. data/app/assets/javascripts/rails_live_dashboard/controllers/tabs_controller.js +37 -0
  13. data/app/assets/stylesheets/rails_live_dashboard/application.css +5 -0
  14. data/app/components/rails_live_dashboard/query_duration_badge_component.html.erb +3 -0
  15. data/app/components/rails_live_dashboard/query_duration_badge_component.rb +27 -0
  16. data/app/components/rails_live_dashboard/request_duration_badge_component.html.erb +3 -0
  17. data/app/components/rails_live_dashboard/request_duration_badge_component.rb +27 -0
  18. data/app/components/rails_live_dashboard/request_method_badge_component.html.erb +6 -0
  19. data/app/components/rails_live_dashboard/request_method_badge_component.rb +24 -0
  20. data/app/components/rails_live_dashboard/request_status_badge_component.html.erb +3 -0
  21. data/app/components/rails_live_dashboard/request_status_badge_component.rb +22 -0
  22. data/app/controllers/rails_live_dashboard/application_controller.rb +4 -0
  23. data/app/controllers/rails_live_dashboard/clean_controller.rb +9 -0
  24. data/app/controllers/rails_live_dashboard/dashboard_controller.rb +8 -0
  25. data/app/controllers/rails_live_dashboard/exceptions_controller.rb +13 -0
  26. data/app/controllers/rails_live_dashboard/queries_controller.rb +11 -0
  27. data/app/controllers/rails_live_dashboard/requests_controller.rb +13 -0
  28. data/app/controllers/rails_live_dashboard/widgets/slowest_queries_controller.rb +11 -0
  29. data/app/controllers/rails_live_dashboard/widgets/slowest_requests_controller.rb +11 -0
  30. data/app/helpers/rails_live_dashboard/application_helper.rb +4 -0
  31. data/app/helpers/rails_live_dashboard/clean_helper.rb +4 -0
  32. data/app/helpers/rails_live_dashboard/dashboard_helper.rb +4 -0
  33. data/app/helpers/rails_live_dashboard/exceptions_helper.rb +4 -0
  34. data/app/helpers/rails_live_dashboard/requests_helper.rb +4 -0
  35. data/app/jobs/rails_live_dashboard/application_job.rb +4 -0
  36. data/app/mailers/rails_live_dashboard/application_mailer.rb +6 -0
  37. data/app/models/rails_live_dashboard/application_record.rb +5 -0
  38. data/app/models/rails_live_dashboard/entry.rb +5 -0
  39. data/app/models/rails_live_dashboard/exception.rb +31 -0
  40. data/app/models/rails_live_dashboard/query.rb +5 -0
  41. data/app/models/rails_live_dashboard/request.rb +45 -0
  42. data/app/models/rails_live_dashboard/types/content.rb +32 -0
  43. data/app/models/rails_live_dashboard/types/exception_content.rb +42 -0
  44. data/app/models/rails_live_dashboard/types/query_content.rb +42 -0
  45. data/app/models/rails_live_dashboard/types/request_content.rb +49 -0
  46. data/app/views/layouts/rails_live_dashboard/application.html.erb +18 -0
  47. data/app/views/rails_live_dashboard/dashboard/show.html.erb +27 -0
  48. data/app/views/rails_live_dashboard/exceptions/_tabs.html.erb +16 -0
  49. data/app/views/rails_live_dashboard/exceptions/index.html.erb +55 -0
  50. data/app/views/rails_live_dashboard/exceptions/show.html.erb +41 -0
  51. data/app/views/rails_live_dashboard/queries/_list.html.erb +43 -0
  52. data/app/views/rails_live_dashboard/queries/index.html.erb +11 -0
  53. data/app/views/rails_live_dashboard/queries/show.html.erb +33 -0
  54. data/app/views/rails_live_dashboard/requests/_contents.html.erb +26 -0
  55. data/app/views/rails_live_dashboard/requests/_exceptions.html.erb +43 -0
  56. data/app/views/rails_live_dashboard/requests/_queries.html.erb +11 -0
  57. data/app/views/rails_live_dashboard/requests/_relateds.html.erb +23 -0
  58. data/app/views/rails_live_dashboard/requests/index.html.erb +48 -0
  59. data/app/views/rails_live_dashboard/requests/show.html.erb +46 -0
  60. data/app/views/rails_live_dashboard/shared/_header.html.erb +43 -0
  61. data/app/views/rails_live_dashboard/widgets/slowest_queries/show.html.erb +42 -0
  62. data/app/views/rails_live_dashboard/widgets/slowest_requests/show.html.erb +42 -0
  63. data/config/routes.rb +14 -0
  64. data/db/migrate/20240213133517_create_rails_live_dashboard_entries.rb +14 -0
  65. data/lib/generators/rails_live_dashboard/install_generator.rb +11 -0
  66. data/lib/generators/templates/initializer.rb +3 -0
  67. data/lib/rails_live_dashboard/configuration.rb +13 -0
  68. data/lib/rails_live_dashboard/context.rb +19 -0
  69. data/lib/rails_live_dashboard/engine.rb +28 -0
  70. data/lib/rails_live_dashboard/recorders/exception_recorder.rb +42 -0
  71. data/lib/rails_live_dashboard/recorders/query_recorder.rb +30 -0
  72. data/lib/rails_live_dashboard/recorders/request_recorder.rb +30 -0
  73. data/lib/rails_live_dashboard/subscribers/action_controller_subscriber.rb +28 -0
  74. data/lib/rails_live_dashboard/subscribers/active_record_subscriber.rb +23 -0
  75. data/lib/rails_live_dashboard/version.rb +3 -0
  76. data/lib/rails_live_dashboard.rb +19 -0
  77. data/lib/tasks/rails_live_dashboard_tasks.rake +4 -0
  78. metadata +136 -0
@@ -0,0 +1,2 @@
1
+ import "@hotwired/turbo-rails";
2
+ import "./controllers";
@@ -0,0 +1,9 @@
1
+ import { Application } from "@hotwired/stimulus";
2
+
3
+ const application = Application.start();
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false;
7
+ window.Stimulus = application;
8
+
9
+ export { application };
@@ -0,0 +1,6 @@
1
+ import { application } from "./application";
2
+ import Reveal from "./reveal_controller";
3
+ import Tabs from "./tabs_controller";
4
+
5
+ application.register("reveal", Reveal);
6
+ application.register("tabs", Tabs);
@@ -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,5 @@
1
+ @import "tailwindcss/base";
2
+
3
+ @import "tailwindcss/components";
4
+
5
+ @import "tailwindcss/utilities";
@@ -0,0 +1,3 @@
1
+ <span class="<%= class_names('inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset', @classes) %>">
2
+ <%= @duration %> ms
3
+ </span>
@@ -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,3 @@
1
+ <span class="<%= class_names('inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset', @classes) %>">
2
+ <%= @duration %> ms
3
+ </span>
@@ -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,3 @@
1
+ <span class="<%= class_names('inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset', @classes) %>">
2
+ <%= @status %>
3
+ </span>
@@ -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,4 @@
1
+ module RailsLiveDashboard
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,9 @@
1
+ module RailsLiveDashboard
2
+ class CleanController < ApplicationController
3
+ def destroy
4
+ Entry.delete_all
5
+
6
+ redirect_to dashboard_path, status: :see_other
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ module RailsLiveDashboard
2
+ class DashboardController < ApplicationController
3
+ def show
4
+ @ruby_version = RUBY_VERSION
5
+ @rails_version = Rails.version
6
+ end
7
+ end
8
+ 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,11 @@
1
+ module RailsLiveDashboard
2
+ class QueriesController < ApplicationController
3
+ def index
4
+ @queries = Query.order(created_at: :desc)
5
+ end
6
+
7
+ def show
8
+ @query = Query.find(params[:id])
9
+ end
10
+ end
11
+ 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,11 @@
1
+ module RailsLiveDashboard
2
+ module Widgets
3
+ class SlowestQueriesController < ApplicationController
4
+ def show
5
+ @queries = Query
6
+ .order(Arel.sql("cast(content->>'duration' as float) DESC"))
7
+ .limit(5)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module RailsLiveDashboard
2
+ module Widgets
3
+ class SlowestRequestsController < ApplicationController
4
+ def show
5
+ @requests = Request
6
+ .order(Arel.sql("cast(content->>'duration' as float) DESC"))
7
+ .limit(5)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ module RailsLiveDashboard
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module RailsLiveDashboard
2
+ module CleanHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module RailsLiveDashboard
2
+ module DashboardHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module RailsLiveDashboard
2
+ module ExceptionsHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module RailsLiveDashboard
2
+ module RequestsHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module RailsLiveDashboard
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module RailsLiveDashboard
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module RailsLiveDashboard
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module RailsLiveDashboard
2
+ class Entry < ApplicationRecord
3
+ scope :should_show, -> { where(should_show: true) }
4
+ end
5
+ 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,5 @@
1
+ module RailsLiveDashboard
2
+ class Query < Entry
3
+ attribute :content, Types::QueryContent.new
4
+ end
5
+ 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>