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.
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>