rails_pulse 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 (160) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +638 -0
  4. data/Rakefile +207 -0
  5. data/app/assets/images/rails_pulse/dashboard.png +0 -0
  6. data/app/assets/images/rails_pulse/menu.svg +1 -0
  7. data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
  8. data/app/assets/images/rails_pulse/request.png +0 -0
  9. data/app/assets/images/rails_pulse/routes.png +0 -0
  10. data/app/assets/stylesheets/rails_pulse/application.css +102 -0
  11. data/app/assets/stylesheets/rails_pulse/components/alert.css +24 -0
  12. data/app/assets/stylesheets/rails_pulse/components/badge.css +58 -0
  13. data/app/assets/stylesheets/rails_pulse/components/base.css +79 -0
  14. data/app/assets/stylesheets/rails_pulse/components/breadcrumb.css +31 -0
  15. data/app/assets/stylesheets/rails_pulse/components/button.css +99 -0
  16. data/app/assets/stylesheets/rails_pulse/components/card.css +19 -0
  17. data/app/assets/stylesheets/rails_pulse/components/chart.css +18 -0
  18. data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +86 -0
  19. data/app/assets/stylesheets/rails_pulse/components/descriptive_list.css +9 -0
  20. data/app/assets/stylesheets/rails_pulse/components/dialog.css +56 -0
  21. data/app/assets/stylesheets/rails_pulse/components/flash.css +47 -0
  22. data/app/assets/stylesheets/rails_pulse/components/input.css +80 -0
  23. data/app/assets/stylesheets/rails_pulse/components/layouts.css +63 -0
  24. data/app/assets/stylesheets/rails_pulse/components/menu.css +43 -0
  25. data/app/assets/stylesheets/rails_pulse/components/popover.css +36 -0
  26. data/app/assets/stylesheets/rails_pulse/components/prose.css +144 -0
  27. data/app/assets/stylesheets/rails_pulse/components/row.css +24 -0
  28. data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +79 -0
  29. data/app/assets/stylesheets/rails_pulse/components/skeleton.css +5 -0
  30. data/app/assets/stylesheets/rails_pulse/components/table.css +37 -0
  31. data/app/assets/stylesheets/rails_pulse/components/utilities.css +36 -0
  32. data/app/controllers/concerns/chart_table_concern.rb +82 -0
  33. data/app/controllers/concerns/response_range_concern.rb +24 -0
  34. data/app/controllers/concerns/time_range_concern.rb +67 -0
  35. data/app/controllers/concerns/zoom_range_concern.rb +40 -0
  36. data/app/controllers/rails_pulse/application_controller.rb +67 -0
  37. data/app/controllers/rails_pulse/assets_controller.rb +33 -0
  38. data/app/controllers/rails_pulse/caches_controller.rb +115 -0
  39. data/app/controllers/rails_pulse/csp_test_controller.rb +57 -0
  40. data/app/controllers/rails_pulse/dashboard_controller.rb +6 -0
  41. data/app/controllers/rails_pulse/operations_controller.rb +219 -0
  42. data/app/controllers/rails_pulse/queries_controller.rb +121 -0
  43. data/app/controllers/rails_pulse/requests_controller.rb +69 -0
  44. data/app/controllers/rails_pulse/routes_controller.rb +99 -0
  45. data/app/helpers/rails_pulse/application_helper.rb +111 -0
  46. data/app/helpers/rails_pulse/breadcrumbs_helper.rb +62 -0
  47. data/app/helpers/rails_pulse/cached_component_helper.rb +73 -0
  48. data/app/helpers/rails_pulse/chart_formatters.rb +43 -0
  49. data/app/helpers/rails_pulse/chart_helper.rb +140 -0
  50. data/app/helpers/rails_pulse/formatting_helper.rb +29 -0
  51. data/app/helpers/rails_pulse/status_helper.rb +279 -0
  52. data/app/helpers/rails_pulse/table_helper.rb +54 -0
  53. data/app/javascript/rails_pulse/application.js +119 -0
  54. data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +20 -0
  55. data/app/javascript/rails_pulse/controllers/context_menu_controller.js +16 -0
  56. data/app/javascript/rails_pulse/controllers/dialog_controller.js +21 -0
  57. data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +67 -0
  58. data/app/javascript/rails_pulse/controllers/form_controller.js +39 -0
  59. data/app/javascript/rails_pulse/controllers/icon_controller.js +170 -0
  60. data/app/javascript/rails_pulse/controllers/index_controller.js +230 -0
  61. data/app/javascript/rails_pulse/controllers/menu_controller.js +60 -0
  62. data/app/javascript/rails_pulse/controllers/pagination_controller.js +69 -0
  63. data/app/javascript/rails_pulse/controllers/popover_controller.js +91 -0
  64. data/app/javascript/rails_pulse/controllers/timezone_controller.js +106 -0
  65. data/app/javascript/rails_pulse/theme.js +416 -0
  66. data/app/jobs/rails_pulse/application_job.rb +4 -0
  67. data/app/jobs/rails_pulse/cleanup_job.rb +21 -0
  68. data/app/mailers/rails_pulse/application_mailer.rb +6 -0
  69. data/app/models/rails_pulse/application_record.rb +7 -0
  70. data/app/models/rails_pulse/component_cache_key.rb +33 -0
  71. data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +27 -0
  72. data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +37 -0
  73. data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +59 -0
  74. data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +45 -0
  75. data/app/models/rails_pulse/operation.rb +87 -0
  76. data/app/models/rails_pulse/queries/cards/average_query_times.rb +52 -0
  77. data/app/models/rails_pulse/queries/cards/execution_rate.rb +57 -0
  78. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +71 -0
  79. data/app/models/rails_pulse/queries/charts/average_query_times.rb +112 -0
  80. data/app/models/rails_pulse/query.rb +58 -0
  81. data/app/models/rails_pulse/request.rb +64 -0
  82. data/app/models/rails_pulse/requests/charts/average_response_times.rb +99 -0
  83. data/app/models/rails_pulse/requests/charts/operations_chart.rb +35 -0
  84. data/app/models/rails_pulse/route.rb +77 -0
  85. data/app/models/rails_pulse/routes/cards/average_response_times.rb +54 -0
  86. data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +73 -0
  87. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +73 -0
  88. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +59 -0
  89. data/app/models/rails_pulse/routes/charts/average_response_times.rb +115 -0
  90. data/app/models/rails_pulse/routes/tables/index.rb +63 -0
  91. data/app/services/rails_pulse/sql_query_normalizer.rb +124 -0
  92. data/app/views/layouts/rails_pulse/_menu_items.html.erb +19 -0
  93. data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +44 -0
  94. data/app/views/layouts/rails_pulse/application.html.erb +72 -0
  95. data/app/views/rails_pulse/caches/show.html.erb +9 -0
  96. data/app/views/rails_pulse/components/_breadcrumbs.html.erb +12 -0
  97. data/app/views/rails_pulse/components/_code_panel.html.erb +12 -0
  98. data/app/views/rails_pulse/components/_metric_card.html.erb +55 -0
  99. data/app/views/rails_pulse/components/_metric_row.html.erb +9 -0
  100. data/app/views/rails_pulse/components/_operation_details_popover.html.erb +241 -0
  101. data/app/views/rails_pulse/components/_panel.html.erb +56 -0
  102. data/app/views/rails_pulse/components/_sparkline_stats.html.erb +15 -0
  103. data/app/views/rails_pulse/components/_table.html.erb +50 -0
  104. data/app/views/rails_pulse/components/_table_head.html.erb +20 -0
  105. data/app/views/rails_pulse/components/_table_pagination.html.erb +45 -0
  106. data/app/views/rails_pulse/components/_time_period.html.erb +16 -0
  107. data/app/views/rails_pulse/csp_test/show.html.erb +207 -0
  108. data/app/views/rails_pulse/dashboard/charts/_bar_chart.html.erb +1 -0
  109. data/app/views/rails_pulse/dashboard/index.html.erb +64 -0
  110. data/app/views/rails_pulse/dashboard/tables/_routes_table.html.erb +32 -0
  111. data/app/views/rails_pulse/dashboard/tables/_standard_table.html.erb +1 -0
  112. data/app/views/rails_pulse/operations/_operation_analysis_application.html.erb +43 -0
  113. data/app/views/rails_pulse/operations/_operation_analysis_database.html.erb +12 -0
  114. data/app/views/rails_pulse/operations/_operation_analysis_generic.html.erb +15 -0
  115. data/app/views/rails_pulse/operations/_operation_analysis_other.html.erb +69 -0
  116. data/app/views/rails_pulse/operations/_operation_analysis_view.html.erb +39 -0
  117. data/app/views/rails_pulse/operations/show.html.erb +79 -0
  118. data/app/views/rails_pulse/queries/_show_table.html.erb +19 -0
  119. data/app/views/rails_pulse/queries/_table.html.erb +31 -0
  120. data/app/views/rails_pulse/queries/index.html.erb +64 -0
  121. data/app/views/rails_pulse/queries/show.html.erb +86 -0
  122. data/app/views/rails_pulse/requests/_operations.html.erb +85 -0
  123. data/app/views/rails_pulse/requests/_table.html.erb +31 -0
  124. data/app/views/rails_pulse/requests/index.html.erb +64 -0
  125. data/app/views/rails_pulse/requests/show.html.erb +44 -0
  126. data/app/views/rails_pulse/routes/_table.html.erb +29 -0
  127. data/app/views/rails_pulse/routes/index.html.erb +65 -0
  128. data/app/views/rails_pulse/routes/show.html.erb +67 -0
  129. data/app/views/rails_pulse/skeletons/_chart.html.erb +3 -0
  130. data/app/views/rails_pulse/skeletons/_metric_card.html.erb +20 -0
  131. data/app/views/rails_pulse/skeletons/_panel.html.erb +19 -0
  132. data/app/views/rails_pulse/skeletons/_table.html.erb +8 -0
  133. data/config/importmap.rb +12 -0
  134. data/config/initializers/rails_charts_csp_patch.rb +83 -0
  135. data/config/initializers/rails_pulse.rb +198 -0
  136. data/config/routes.rb +16 -0
  137. data/db/migrate/20250227235904_create_routes.rb +12 -0
  138. data/db/migrate/20250227235915_create_requests.rb +19 -0
  139. data/db/migrate/20250228000000_create_queries.rb +14 -0
  140. data/db/migrate/20250228000056_create_operations.rb +24 -0
  141. data/lib/generators/rails_pulse/install_generator.rb +17 -0
  142. data/lib/generators/rails_pulse/templates/rails_pulse.rb +198 -0
  143. data/lib/rails_pulse/cleanup_service.rb +212 -0
  144. data/lib/rails_pulse/configuration.rb +176 -0
  145. data/lib/rails_pulse/engine.rb +88 -0
  146. data/lib/rails_pulse/middleware/asset_server.rb +84 -0
  147. data/lib/rails_pulse/middleware/request_collector.rb +120 -0
  148. data/lib/rails_pulse/migration.rb +29 -0
  149. data/lib/rails_pulse/subscribers/operation_subscriber.rb +280 -0
  150. data/lib/rails_pulse/version.rb +3 -0
  151. data/lib/rails_pulse.rb +38 -0
  152. data/lib/tasks/rails_pulse_tasks.rake +138 -0
  153. data/public/rails-pulse-assets/csp-test.js +110 -0
  154. data/public/rails-pulse-assets/rails-pulse-icons.js +89 -0
  155. data/public/rails-pulse-assets/rails-pulse-icons.js.map +13 -0
  156. data/public/rails-pulse-assets/rails-pulse.css +1 -0
  157. data/public/rails-pulse-assets/rails-pulse.css.map +1 -0
  158. data/public/rails-pulse-assets/rails-pulse.js +183 -0
  159. data/public/rails-pulse-assets/rails-pulse.js.map +7 -0
  160. metadata +339 -0
@@ -0,0 +1,44 @@
1
+ <div class="sidebar-menu">
2
+ <a class="btn sidebar-menu__button" href="#">
3
+ <div class="flex shrink-0">
4
+ <%#= image_tag 'rails_pulse/rails-pulse-logo.png', width: '28px' %>
5
+ </div>
6
+ <div class="flex flex-col text-start leading-tight overflow-hidden">
7
+ <span class="overflow-ellipsis font-semibold">Rails Pulse</span>
8
+ <span class="overflow-ellipsis text-xs">Open Source</span>
9
+ </div>
10
+ </a>
11
+
12
+ <div class="sidebar-menu__content">
13
+ <div class="sidebar-menu__group">
14
+ <nav class="sidebar-menu__items">
15
+ <%= link_to root_path, class: 'btn sidebar-menu__button' do %>
16
+ <%= rails_pulse_icon 'layout-dashboard', width: '16' %>
17
+ <span class="overflow-ellipsis">Dashboard</span>
18
+ <% end %>
19
+
20
+ <%= link_to routes_path, class: 'btn sidebar-menu__button' do %>
21
+ <%= rails_pulse_icon 'route', width: '16' %>
22
+ <span class="overflow-ellipsis">Routes</span>
23
+ <% end %>
24
+
25
+ <%= link_to requests_path, class: 'btn sidebar-menu__button' do %>
26
+ <%= rails_pulse_icon 'audio-lines', width: '16' %>
27
+ <span class="overflow-ellipsis">Requests</span>
28
+ <% end %>
29
+
30
+ <%= link_to queries_path, class: 'btn sidebar-menu__button' do %>
31
+ <%= rails_pulse_icon 'database', width: '16' %>
32
+ <span class="overflow-ellipsis">Queries</span>
33
+ <% end %>
34
+ </nav>
35
+ </div>
36
+ </div>
37
+
38
+ <a class="btn sidebar-menu__button mbs-auto" href="https://www.railspulse.com" target="_blank" rel="noopener noreferrer">
39
+ <div class="flex flex-col text-start leading-tight overflow-hidden">
40
+ <span class="overflow-ellipsis max-i-full font-semibold">Rails Pulse</span>
41
+ <span class="overflow-ellipsis max-i-full text-xs">www.railspulse.com</span>
42
+ </div>
43
+ </a>
44
+ </div>
@@ -0,0 +1,72 @@
1
+ <!DOCTYPE html>
2
+ <html data-color-scheme="light">
3
+ <head>
4
+ <title><%= content_for(:title) || "Rails Pulse" %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <meta name="mobile-web-app-capable" content="yes">
8
+ <%= csrf_meta_tags %>
9
+ <%= csp_meta_tag %>
10
+
11
+ <%= yield :head %>
12
+
13
+ <!-- Rails Pulse Pre-compiled Assets (CSP-safe) -->
14
+ <%= stylesheet_link_tag rails_pulse.asset_path('rails-pulse.css'), 'data-turbo-track': 'reload' %>
15
+ <%= javascript_include_tag rails_pulse.asset_path('rails-pulse-icons.js'), 'data-turbo-track': 'reload', defer: true, nonce: rails_pulse_csp_nonce %>
16
+ <%= javascript_include_tag rails_pulse.asset_path('rails-pulse.js'), 'data-turbo-track': 'reload', defer: true, nonce: rails_pulse_csp_nonce %>
17
+ </head>
18
+
19
+ <body class="header-layout">
20
+ <header id="header">
21
+ <div class="flex justify-between container">
22
+ <div class="hide@md" data-controller="rails-pulse--dialog">
23
+ <button type="button" class="btn btn--icon" data-action="rails-pulse--dialog#showModal">
24
+ <%= rails_pulse_icon 'menu', width: '20' %>
25
+ <span class="sr-only">Open menu</span>
26
+ </button>
27
+
28
+ <dialog class="sheet sheet--left" style="--sheet-size: 288px;" data-rails-pulse--dialog-target="menu" data-action="click->rails-pulse--dialog#closeOnClickOutside">
29
+ <div class="sheet__content p-2">
30
+ <div class="sidebar-menu">
31
+ <a class="btn sidebar-menu__button" href="#">
32
+ <div class="flex flex-col text-start leading-tight overflow-hidden">
33
+ <span class="overflow-ellipsis font-semibold">Rails Pulse</span>
34
+ <span class="overflow-ellipsis text-xs">Open Source</span>
35
+ </div>
36
+ </a>
37
+ <div class="sidebar-menu__content">
38
+ <div class="sidebar-menu__group">
39
+ <nav class="sidebar-menu__items">
40
+ <%= render 'layouts/rails_pulse/menu_items' %>
41
+ </nav>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </dialog>
47
+ </div>
48
+
49
+ <div class="flex items-center gap">
50
+ <a class="flex items-center gap mie-2" href="#">
51
+ <span class="font-bold">Rails Pulse</span>
52
+ </a>
53
+ <nav class="flex items-center gap text-sm text-subtle show@md" style="--column-gap: 1rem">
54
+ <%= render 'layouts/rails_pulse/menu_items' %>
55
+ </nav>
56
+ </div>
57
+
58
+ <div class="flex items-center" data-controller="rails-pulse--color-scheme">
59
+ <%= link_to '#', "aria-label": "Toggle color scheme", role: "button", data: { action: "rails-pulse--color-scheme#toggle" } do %>
60
+ <%= rails_pulse_icon 'sun', width: '48' %>
61
+ <% end %>
62
+ </div>
63
+ </header>
64
+
65
+ <main id="main">
66
+ <div class="container flex flex-col gap">
67
+ <%= yield %>
68
+ </div>
69
+ </main>
70
+
71
+ </body>
72
+ </html>
@@ -0,0 +1,9 @@
1
+ <%
2
+ id = "#{@component_id}_#{@component_options[:component]}"
3
+ %>
4
+ <%= turbo_frame_tag id do %>
5
+ <%= render partial: "rails_pulse/components/#{@component_options[:component]}", locals: @component_options %>
6
+ <script nonce="<%= rails_pulse_csp_nonce %>">
7
+ initializeChartsInContainer('<%= id %>');
8
+ </script>
9
+ <% end %>
@@ -0,0 +1,12 @@
1
+ <nav class="breadcrumb" aria-label="Breadcrumb">
2
+ <% breadcrumbs.each_with_index do |crumb, index| %>
3
+ <% if crumb[:current] %>
4
+ <span class="text-primary" aria-disabled="true" aria-current="page" role="link"><%= crumb[:title] %></span>
5
+ <% else %>
6
+ <%= link_to crumb[:title], crumb[:path] %>
7
+ <% end %>
8
+ <% unless index == breadcrumbs.length - 1 %>
9
+ <%= rails_pulse_icon 'chevron-right', width: 14, height: 14, class: "breadcrumb-separator mbe-1" %>
10
+ <% end %>
11
+ <% end %>
12
+ </nav>
@@ -0,0 +1,12 @@
1
+ <%
2
+ title ||= nil
3
+ %>
4
+
5
+ <div>
6
+ <% if title %>
7
+ <h2 class="grow font-semibold leading-none mbe-1 uppercase text-xs"><%= title %></h2>
8
+ <% end %>
9
+ <div class="prose max-i-none">
10
+ <pre class="mbs-0" style="white-space: normal; margin-block-end: 0; margin-block-start: 0;"><code><%= html_escape(value) %></code></pre>
11
+ </div>
12
+ </div>
@@ -0,0 +1,55 @@
1
+ <%
2
+ @component_data ||= {}
3
+ id ||= @component_data[:id]
4
+ context ||= @component_data[:context]
5
+ title ||= @component_data[:title]
6
+ summary ||= @component_data[:summary]
7
+ line_chart_data ||= @component_data[:line_chart_data]
8
+ trend_icon ||= @component_data[:trend_icon]
9
+ trend_amount ||= @component_data[:trend_amount]
10
+ trend_text ||= @component_data[:trend_text]
11
+ %>
12
+ <div class="grid-item" data-controller="rails-pulse--chart">
13
+ <%= render 'rails_pulse/components/panel', { title: title, card_classes: 'card-compact' } do %>
14
+ <div class="row mbs-2" style="height: 50px; margin-bottom: 0;">
15
+ <div class="grid-item">
16
+ <h4 class="text-xl mbs-1 font-bold"><%= summary %></h4>
17
+ </div>
18
+ <div class="grid-item">
19
+ <%= line_chart line_chart_data, height: "100%", options: sparkline_chart_options %>
20
+ </div>
21
+ </div>
22
+ <div class="mbs-2" style="height: 10px;">
23
+ <%
24
+ badge =
25
+ case trend_icon
26
+ when "trending-up" then "badge--negative-inverse"
27
+ when "trending-down" then "badge--positive-inverse"
28
+ else
29
+ "badge--primary-inverse"
30
+ end
31
+ %>
32
+ <div class="flex items-center justify-between">
33
+ <span class="badge <%= badge %> p-0">
34
+ <%= rails_pulse_icon trend_icon, height: "15px", width: "15px", class: "mie-2" %>
35
+ <%= trend_amount %>
36
+ <p class="mis-2 text-subtle text-xs"><%= trend_text %></p>
37
+ </span>
38
+ <%
39
+ refresh_path = rails_pulse.cache_path(id: id, context: context, refresh: true)
40
+ cached_iso = @cached_at ? @cached_at.iso8601 : nil
41
+ %>
42
+ <%= link_to refresh_path,
43
+ data: {
44
+ controller: "rails-pulse--timezone",
45
+ rails_pulse__timezone_target_frame_value: "#{id}_card",
46
+ rails_pulse__timezone_cached_at_value: cached_iso,
47
+ turbo_frame: "#{id}_card",
48
+ turbo_prefetch: "false"
49
+ } do %>
50
+ <%= rails_pulse_icon 'refresh-cw', height: "15px", width: "15px" %>
51
+ <% end %>
52
+ </div>
53
+ </div>
54
+ <% end %>
55
+ </div>
@@ -0,0 +1,9 @@
1
+ <% value ||= nil %>
2
+ <div>
3
+ <h5 class="text-subtle text-md"><%= title %></h5>
4
+ <% if value %>
5
+ <p class="text-md mb-0"><%= value %></p>
6
+ <% else %>
7
+ <%= yield %>
8
+ <% end %>
9
+ </div>
@@ -0,0 +1,241 @@
1
+ <div
2
+ popover
3
+ class="popover card operation-details-popover"
4
+ data-rails-pulse--popover-target="menu"
5
+ style="--popover-size: min(70rem); max-inline-size: 70rem;"
6
+ >
7
+ <div class="flex flex-col gap">
8
+ <div class="flex items-center justify-between">
9
+ <h3 class="text-sm font-semibold leading-none uppercase text-subtle">
10
+ <%= operation.operation_type.upcase %> Operation
11
+ </h3>
12
+ <span class="badge" style="background-color: <%= event_color(operation.operation_type) %>15; color: <%= event_color(operation.operation_type) %>;">
13
+ <%= operation.duration.round(2) %>ms
14
+ </span>
15
+ </div>
16
+
17
+ <div class="flex flex-col gap-half">
18
+ <div>
19
+ <h4 class="text-xs font-medium text-subtle uppercase">Label</h4>
20
+ <div class="text-sm p-2 bg-shade rounded-sm border break-words" style="font-family: monospace;">
21
+ <%= html_escape(operation.label) %>
22
+ </div>
23
+ </div>
24
+
25
+ <% if operation.occurred_at.present? %>
26
+ <div>
27
+ <h4 class="text-xs font-medium text-subtle uppercase">Occurred At</h4>
28
+ <div class="text-sm">
29
+ <%= operation.occurred_at.strftime("%H:%M:%S.%L") %>
30
+ </div>
31
+ </div>
32
+ <% end %>
33
+
34
+ <% case operation.operation_type %>
35
+ <% when "sql" %>
36
+ <div>
37
+ <h4 class="text-xs font-medium text-subtle uppercase">Performance Notes</h4>
38
+ <div class="text-sm flex flex-col gap-half">
39
+ <% if operation.duration > 100 %>
40
+ <div class="text-negative"><%= rails_pulse_icon 'triangle-alert', width: '16', class: 'inline-block' %> Slow query (>100ms)</div>
41
+ <% elsif operation.duration > 50 %>
42
+ <div style="color: #d97706;">⚡ Moderate query (>50ms)</div>
43
+ <% else %>
44
+ <div class="text-positive"><%= rails_pulse_icon 'check', width: '16', class: 'inline-block' %> Fast query</div>
45
+ <% end %>
46
+
47
+ <% if operation.query.present? %>
48
+ <div class="text-xs text-subtle">
49
+ Query normalized for tracking
50
+ </div>
51
+ <% end %>
52
+ </div>
53
+ </div>
54
+
55
+ <% if operation.label&.match?(/SELECT.*FROM\s+(\w+)/i) %>
56
+ <div>
57
+ <h4 class="text-xs font-medium text-subtle uppercase">Table</h4>
58
+ <div class="text-sm p-2 bg-shade rounded-sm border break-words">
59
+ <%= html_escape(operation.label.match(/FROM\s+(\w+)/i)&.captures&.first || "Unknown") %>
60
+ </div>
61
+ </div>
62
+ <% end %>
63
+
64
+ <% when "controller" %>
65
+ <div>
66
+ <h4 class="text-xs font-medium text-subtle uppercase">Action Info</h4>
67
+ <div class="text-sm flex flex-col gap-half">
68
+ <% if operation.duration > 500 %>
69
+ <div class="text-negative"><%= rails_pulse_icon 'triangle-alert', width: '16', class: 'inline-block' %> Slow action (>500ms)</div>
70
+ <% elsif operation.duration > 200 %>
71
+ <div style="color: #d97706;">⚡ Moderate action (>200ms)</div>
72
+ <% else %>
73
+ <div class="text-positive"><%= rails_pulse_icon 'check', width: '16', class: 'inline-block' %> Fast action</div>
74
+ <% end %>
75
+ <div class="text-xs text-subtle">
76
+ Controller action processing time
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <% when "template", "partial", "layout", "collection" %>
82
+ <div>
83
+ <h4 class="text-xs font-medium text-subtle uppercase">View Performance</h4>
84
+ <div class="text-sm flex flex-col gap-half">
85
+ <% if operation.duration > 100 %>
86
+ <div class="text-negative"><%= rails_pulse_icon 'triangle-alert', width: '16', class: 'inline-block' %> Slow render (>100ms)</div>
87
+ <% elsif operation.duration > 50 %>
88
+ <div style="color: #d97706;">⚡ Moderate render (>50ms)</div>
89
+ <% else %>
90
+ <div class="text-positive"><%= rails_pulse_icon 'check', width: '16', class: 'inline-block' %> Fast render</div>
91
+ <% end %>
92
+ <div class="text-xs text-subtle">
93
+ <%= operation.operation_type.capitalize %> rendering time
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <% if operation.codebase_location.present? %>
99
+ <div>
100
+ <h4 class="text-xs font-medium text-subtle uppercase">File Location</h4>
101
+ <div class="text-xs p-2 bg-shade rounded-sm border break-words" style="font-family: monospace;">
102
+ <%= html_escape(operation.codebase_location.split('/').last(3).join('/')) %>
103
+ </div>
104
+ </div>
105
+ <% end %>
106
+
107
+ <% when "cache_read", "cache_write" %>
108
+ <div>
109
+ <h4 class="text-xs font-medium text-subtle uppercase">Cache Performance</h4>
110
+ <div class="text-sm flex flex-col gap-half">
111
+ <% if operation.operation_type == "cache_read" %>
112
+ <% if operation.duration > 10 %>
113
+ <div style="color: #d97706;">⚡ Slow cache read (>10ms)</div>
114
+ <% else %>
115
+ <div class="text-positive"><%= rails_pulse_icon 'check', width: '16', class: 'inline-block' %> Fast cache read</div>
116
+ <% end %>
117
+ <% else %>
118
+ <% if operation.duration > 50 %>
119
+ <div style="color: #d97706;">⚡ Slow cache write (>50ms)</div>
120
+ <% else %>
121
+ <div class="text-positive"><%= rails_pulse_icon 'check', width: '16', class: 'inline-block' %> Fast cache write</div>
122
+ <% end %>
123
+ <% end %>
124
+ <div class="text-xs text-subtle">
125
+ <%= operation.operation_type.humanize %> operation
126
+ </div>
127
+ </div>
128
+ </div>
129
+
130
+ <% when "http" %>
131
+ <div>
132
+ <h4 class="text-xs font-medium text-subtle uppercase">HTTP Request</h4>
133
+ <div class="text-sm flex flex-col gap-half">
134
+ <% if operation.duration > 1000 %>
135
+ <div class="text-negative"><%= rails_pulse_icon 'triangle-alert', width: '16', class: 'inline-block' %> Very slow request (>1s)</div>
136
+ <% elsif operation.duration > 500 %>
137
+ <div style="color: #d97706;">⚡ Slow request (>500ms)</div>
138
+ <% else %>
139
+ <div class="text-positive"><%= rails_pulse_icon 'check', width: '16', class: 'inline-block' %> Fast request</div>
140
+ <% end %>
141
+ <div class="text-xs text-subtle">
142
+ External HTTP request duration
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ <% when "job" %>
148
+ <div>
149
+ <h4 class="text-xs font-medium text-subtle uppercase">Background Job</h4>
150
+ <div class="text-sm flex flex-col gap-half">
151
+ <% if operation.duration > 5000 %>
152
+ <div class="text-negative"><%= rails_pulse_icon 'triangle-alert', width: '16', class: 'inline-block' %> Long-running job (>5s)</div>
153
+ <% elsif operation.duration > 1000 %>
154
+ <div style="color: #d97706;">⚡ Moderate job (>1s)</div>
155
+ <% else %>
156
+ <div class="text-positive"><%= rails_pulse_icon 'check', width: '16', class: 'inline-block' %> Quick job</div>
157
+ <% end %>
158
+ <div class="text-xs text-subtle">
159
+ Active Job execution time
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ <% when "mailer" %>
165
+ <div>
166
+ <h4 class="text-xs font-medium text-subtle uppercase">Email Delivery</h4>
167
+ <div class="text-sm flex flex-col gap-half">
168
+ <% if operation.duration > 2000 %>
169
+ <div class="text-negative"><%= rails_pulse_icon 'triangle-alert', width: '16', class: 'inline-block' %> Slow email delivery (>2s)</div>
170
+ <% elsif operation.duration > 500 %>
171
+ <div style="color: #d97706;">⚡ Moderate delivery (>500ms)</div>
172
+ <% else %>
173
+ <div class="text-positive"><%= rails_pulse_icon 'check', width: '16', class: 'inline-block' %> Fast delivery</div>
174
+ <% end %>
175
+ <div class="text-xs text-subtle">
176
+ ActionMailer delivery time
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ <% when "storage" %>
182
+ <div>
183
+ <h4 class="text-xs font-medium text-subtle uppercase">File Storage</h4>
184
+ <div class="text-sm flex flex-col gap-half">
185
+ <% if operation.duration > 1000 %>
186
+ <div class="text-negative"><%= rails_pulse_icon 'triangle-alert', width: '16', class: 'inline-block' %> Slow storage operation (>1s)</div>
187
+ <% elsif operation.duration > 500 %>
188
+ <div style="color: #d97706;">⚡ Moderate storage (>500ms)</div>
189
+ <% else %>
190
+ <div class="text-positive"><%= rails_pulse_icon 'check', width: '16', class: 'inline-block' %> Fast storage</div>
191
+ <% end %>
192
+ <div class="text-xs text-subtle">
193
+ Active Storage operation time
194
+ </div>
195
+ </div>
196
+ </div>
197
+
198
+ <% else %>
199
+ <div>
200
+ <h4 class="text-xs font-medium text-subtle uppercase">Performance</h4>
201
+ <div class="text-sm">
202
+ <% if operation.duration > 100 %>
203
+ <div style="color: #d97706;">⚡ Duration: <%= operation.duration.round(2) %>ms</div>
204
+ <% else %>
205
+ <div class="text-positive"><%= rails_pulse_icon 'check', width: '16', class: 'inline-block' %> Duration: <%= operation.duration.round(2) %>ms</div>
206
+ <% end %>
207
+ </div>
208
+ </div>
209
+ <% end %>
210
+
211
+ <% if operation.codebase_location.present? && !%w[template partial layout].include?(operation.operation_type) %>
212
+ <div>
213
+ <h4 class="text-xs font-medium text-subtle uppercase">Source Location</h4>
214
+ <div class="text-xs p-2 bg-shade rounded-sm border break-words" style="font-family: monospace;">
215
+ <%= html_escape(operation.codebase_location.split('/').last(3).join('/')) %>
216
+ </div>
217
+ </div>
218
+ <% end %>
219
+ </div>
220
+
221
+ <div class="border-bs pbs-3">
222
+ <div class="text-xs text-subtle">
223
+ 💡 <strong>Tips:</strong>
224
+ <% case operation.operation_type %>
225
+ <% when "sql" %>
226
+ Check for missing indexes, N+1 queries, or overly complex joins.
227
+ <% when "controller" %>
228
+ Look for heavy computation, multiple database calls, or inefficient logic.
229
+ <% when "template", "partial", "layout" %>
230
+ Consider caching, reducing database calls in views, or simplifying logic.
231
+ <% when "http" %>
232
+ Consider caching responses, using background jobs, or optimizing external service calls.
233
+ <% when "cache_read", "cache_write" %>
234
+ Review cache key complexity and storage backend performance.
235
+ <% else %>
236
+ Monitor this operation for performance patterns and consider optimization.
237
+ <% end %>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </div>
@@ -0,0 +1,56 @@
1
+ <%
2
+ title ||= nil
3
+ help_heading ||= nil
4
+ help_text ||= nil
5
+ link_to ||= nil
6
+ actions ||= nil
7
+ card_classes ||= ""
8
+ content_partial ||= nil
9
+ %>
10
+ <div class="card <%= card_classes %>">
11
+ <div class="flex flex-col b-full">
12
+ <div class="flex">
13
+ <% if title %>
14
+ <h2 class="grow font-semibold leading-none mbe-1 uppercase text-xs"><%= title %></h2>
15
+ <% end %>
16
+ <div class="flex gap items-center">
17
+ <% if help_heading %>
18
+ <div data-controller="rails-pulse--popover" data-rails-pulse--popover-placement-value="bottom-end">
19
+ <a href="#"
20
+ data-rails-pulse--popover-target="button"
21
+ data-action="rails-pulse--popover#toggle"
22
+ data-popovertarget="popover">
23
+ <%= rails_pulse_icon 'message-circle-question', height: "20px" %>
24
+ </a>
25
+
26
+ <div popover class="popover card" data-rails-pulse--popover-target="menu" style="max-width: 20rem">
27
+ <div class="flex flex-col">
28
+ <h3 class="font-semibold leading-none mbe-2 uppercase text-sm"><%= help_heading %></h3>
29
+ <p class="text-sm text-subtle"><%= help_text %></p>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ <% end %>
34
+ <% if actions %>
35
+ <% actions.each do |action| %>
36
+ <%= link_to action[:url],
37
+ title: action[:title] || "",
38
+ class: action[:class] || "",
39
+ data: action[:data] || {} do %>
40
+ <%= rails_pulse_icon action[:icon] || 'external-link', height: "20px" %>
41
+ <% end %>
42
+ <% end %>
43
+ <% elsif link_to %>
44
+ <%= link_to link_to, title: "View details", class: "" do %>
45
+ <%= rails_pulse_icon 'external-link', height: "20px" %>
46
+ <% end %>
47
+ <% end %>
48
+ </div>
49
+ </div>
50
+ <% if content_partial %>
51
+ <%= render content_partial %>
52
+ <% else %>
53
+ <%= yield %>
54
+ <% end %>
55
+ </div>
56
+ </div>
@@ -0,0 +1,15 @@
1
+ <div class="row mbs-2" style="height: 50px; margin-bottom: 0;">
2
+ <div class="grid-item">
3
+ <h4 class="text-xl mbs-1 font-bold"><%= summary %></h4>
4
+ </div>
5
+ <div class="grid-item">
6
+ <%= line_chart line_chart_data, height: "100%", options: sparkline_chart_options %>
7
+ </div>
8
+ </div>
9
+ <div>
10
+ <span class="badge badge--<%= trend_direction == "down" ? "positive" : "negative" %>-inverse p-0">
11
+ <%= rails_pulse_icon "trending-#{trend_direction}", height: "15px" %>
12
+ <%= trend_amount %>
13
+ <p class="mis-2 text-subtle text-xs"><%= trend_text %></p>
14
+ </span>
15
+ </div>
@@ -0,0 +1,50 @@
1
+ <%
2
+ # Extract the table data from the arguments
3
+ table_data = defined?(table_data) ? table_data : (defined?(data) ? data : {})
4
+
5
+ # Handle both old format (array) and new format (hash with columns and data)
6
+ if table_data.is_a?(Array)
7
+ # Legacy format - assume route data
8
+ columns = [
9
+ { field: :route_path, label: 'Route' },
10
+ { field: :this_week_avg, label: 'This Week Avg (ms)', highlight: :trend },
11
+ { field: :last_week_avg, label: 'Last Week Avg (ms)' },
12
+ { field: :percentage_change, label: 'Change', format: :percentage, highlight: :percentage_change },
13
+ { field: :request_count, label: 'Requests' }
14
+ ]
15
+ data_rows = table_data
16
+ else
17
+ # New format - dynamic columns and data
18
+ columns = table_data[:columns] || []
19
+ data_rows = table_data[:data] || []
20
+ end
21
+
22
+ colspan = columns.length
23
+ %>
24
+
25
+ <table class="table mbs-4">
26
+ <thead>
27
+ <tr>
28
+ <% columns.each do |column| %>
29
+ <th class="<%= column[:class] %>"><%= column[:label] %></th>
30
+ <% end %>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ <% if data_rows.any? %>
35
+ <% data_rows.each do |row_data| %>
36
+ <tr>
37
+ <% columns.each do |column| %>
38
+ <td class="<%= column[:cell_class] %> <%= cell_highlight_class(row_data, column) %>">
39
+ <%= render_cell_content(row_data, column) %>
40
+ </td>
41
+ <% end %>
42
+ </tr>
43
+ <% end %>
44
+ <% else %>
45
+ <tr>
46
+ <td colspan="<%= colspan %>" class="text-center text-subtle">No data available for the selected time period</td>
47
+ </tr>
48
+ <% end %>
49
+ </tbody>
50
+ </table>
@@ -0,0 +1,20 @@
1
+ <thead>
2
+ <tr>
3
+ <% columns.each do |column| %>
4
+ <th class="<%= column[:class] %>">
5
+ <% if column[:sortable] == false %>
6
+ <%= column[:label] %>
7
+ <% else %>
8
+ <% sort_params = {} %>
9
+ <% sort_params[:zoom_start_time] = @zoom_start if @zoom_start.present? %>
10
+ <% sort_params[:zoom_end_time] = @zoom_end if @zoom_end.present? %>
11
+ <%= sort_link @ransack_query, column[:field], column[:label],
12
+ sort_params.merge(
13
+ class: "flex items-center",
14
+ data: { turbo_prefetch: "false", turbo_action: "replace" }
15
+ ) %>
16
+ <% end %>
17
+ </th>
18
+ <% end %>
19
+ </tr>
20
+ </thead>
@@ -0,0 +1,45 @@
1
+ <div class="flex items-center mbs-4">
2
+ <div class="text-sm text-subtle show@md">Total of <%= @pagy.count %> record(s).</div>
3
+
4
+ <div class="flex items-center mis-auto justify-end" style="column-gap: 1rem">
5
+ <div class="flex items-center gap show@md"
6
+ data-controller="rails-pulse--pagination"
7
+ data-rails-pulse--pagination-url-value="<%= rails_pulse.pagination_limit_path %>">
8
+ <label for="pagination_limit" class="text-sm font-medium">Rows per page</label>
9
+ <%= select_tag :limit,
10
+ options_for_select([[10, 10], [20, 20], [30, 30], [40, 40], [50, 50]], @pagy.vars[:items]),
11
+ {
12
+ id: "pagination_limit",
13
+ class: "input",
14
+ style: "--input-inline-size: 70px",
15
+ data: {
16
+ "rails-pulse--pagination-target": "limit",
17
+ "rails-pulse--index-target": "paginationLimit",
18
+ action: "change->rails-pulse--pagination#updateLimit"
19
+ }
20
+ }
21
+ %>
22
+ </div>
23
+
24
+ <div class="text-sm font-medium"><%= "Page #{@pagy.page} of #{@pagy.pages}" %></div>
25
+
26
+ <nav class="flex items-center gap shrink-0" style="--btn-padding: .5rem;" aria-label="Pagination">
27
+ <%= link_to pagy_url_for(@pagy, 1), class: "btn", aria: { disabled: @pagy.prev.nil? }.compact_blank do %>
28
+ <%= rails_pulse_icon 'chevrons-left', width: '16', height: '16' %>
29
+ <span class="sr-only">Go to first page</span>
30
+ <% end %>
31
+ <%= link_to pagy_url_for(@pagy, @pagy.prev || @pagy.page), class: "btn", aria: { disabled: @pagy.prev.nil? }.compact_blank do %>
32
+ <%= rails_pulse_icon 'chevron-left', width: '16', height: '16' %>
33
+ <span class="sr-only">Go to previous page</span>
34
+ <% end %>
35
+ <%= link_to pagy_url_for(@pagy, @pagy.next || @pagy.page), class: "btn", aria: { disabled: @pagy.next.nil? }.compact_blank do %>
36
+ <%= rails_pulse_icon 'chevron-right', width: '16', height: '16' %>
37
+ <span class="sr-only">Go to next page</span>
38
+ <% end %>
39
+ <%= link_to pagy_url_for(@pagy, @pagy.last), class: "btn", aria: { disabled: @pagy.next.nil? }.compact_blank do %>
40
+ <%= rails_pulse_icon 'chevrons-right', width: '16', height: '16' %>
41
+ <span class="sr-only">Go to last page</span>
42
+ <% end %>
43
+ </nav>
44
+ </div>
45
+ </div>