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,16 @@
1
+ <div class="flex gap items-center justify-end mbe-4">
2
+ <p>Time period</p>
3
+ <div data-controller="rails-pulse--popover" data-rails-pulse--popover-placement-value="bottom-start">
4
+ <button type="button" id="menu_button" class="btn" data-rails-pulse--popover-target="button" data-action="rails-pulse--popover#toggle" aria-haspopup="true" aria-controls="menu">
5
+ 1 month
6
+ <%= rails_pulse_icon 'chevron-down' %>
7
+ </button>
8
+ <div popover class="popover" style="--popover-size: 8rem;" data-rails-pulse--popover-target="menu">
9
+ <div id="menu" class="menu" data-controller="rails-pulse--menu" data-action="keydown.up->rails-pulse--menu#prev keydown.down->rails-pulse--menu#next" role="menu" aria-labelledby="menu_button">
10
+ <a class="btn menu__item" data-rails-pulse--menu-target="item" role="menuitem" href="">1 week</a>
11
+ <a class="btn menu__item" data-rails-pulse--menu-target="item" role="menuitem" href="">1 month</a>
12
+ <a class="btn menu__item" data-rails-pulse--menu-target="item" role="menuitem" href="">3 months</a>
13
+ </div>
14
+ </div>
15
+ </div>
16
+ </div>
@@ -0,0 +1,207 @@
1
+ <% content_for :title, "CSP Compliance Test" %>
2
+
3
+ <div class="p-6">
4
+ <h1 class="text-2xl font-bold mb-4">Content Security Policy Compliance Test</h1>
5
+
6
+ <p class="text-subtle mb-6">This page tests Rails Pulse components under strict CSP policies to ensure all assets and functionality work safely.</p>
7
+
8
+ <div class="flex flex-col gap">
9
+
10
+ <!-- Icon Controller Test -->
11
+ <div class="card">
12
+ <div class="p-4">
13
+ <h2 class="text-lg font-semibold mb-3">Icon Controller Test</h2>
14
+ <p class="text-subtle mb-4">Testing CSP-safe icon rendering with pre-compiled assets:</p>
15
+
16
+ <div class="flex gap items-center mb-3">
17
+ <span
18
+ data-controller="rails-pulse--icon"
19
+ data-rails-pulse--icon-name-value="menu"
20
+ data-rails-pulse--icon-width-value="24"
21
+ data-rails-pulse--icon-height-value="24"
22
+ class="loading error loaded"
23
+ ></span>
24
+ <span class="mis-2">Menu Icon</span>
25
+ </div>
26
+
27
+ <div class="flex gap items-center mb-3">
28
+ <span
29
+ data-controller="rails-pulse--icon"
30
+ data-rails-pulse--icon-name-value="check"
31
+ data-rails-pulse--icon-width-value="20"
32
+ data-rails-pulse--icon-height-value="20"
33
+ ></span>
34
+ <span class="mis-2">Check Icon</span>
35
+ </div>
36
+
37
+ <div class="flex gap items-center">
38
+ <span
39
+ data-controller="rails-pulse--icon"
40
+ data-rails-pulse--icon-name-value="nonexistent"
41
+ data-rails-pulse--icon-width-value="16"
42
+ data-rails-pulse--icon-height-value="16"
43
+ ></span>
44
+ <span class="mis-2">Missing Icon (should show placeholder)</span>
45
+ </div>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Popover Controller Test -->
50
+ <div class="card">
51
+ <div class="p-4">
52
+ <h2 class="text-lg font-semibold mb-3">Popover Controller Test</h2>
53
+ <p class="text-subtle mb-4">Testing CSP-safe popover positioning:</p>
54
+
55
+ <div data-controller="rails-pulse--popover">
56
+ <button
57
+ data-rails-pulse--popover-target="button"
58
+ data-action="click->rails-pulse--popover#toggle"
59
+ class="btn btn--primary"
60
+ >
61
+ Show Popover
62
+ </button>
63
+
64
+ <div
65
+ data-rails-pulse--popover-target="menu"
66
+ popover="auto"
67
+ class="bg-white border rounded-md shadow-md p-3"
68
+ >
69
+ <p class="mb-2">This popover uses CSS custom properties for positioning (CSP-safe)</p>
70
+ <p class="text-subtle">No inline styles are used.</p>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- Context Menu Controller Test -->
77
+ <div class="card">
78
+ <div class="p-4">
79
+ <h2 class="text-lg font-semibold mb-3">Context Menu Controller Test</h2>
80
+ <p class="text-subtle mb-4">Testing CSP-safe context menu positioning:</p>
81
+
82
+ <div data-controller="rails-pulse--context-menu">
83
+ <div
84
+ class="card p-4 border border-dark bg-shade text-center"
85
+ data-action="contextmenu->rails-pulse--context-menu#show"
86
+ >
87
+ Right-click me to show context menu
88
+ </div>
89
+
90
+ <div
91
+ data-rails-pulse--context-menu-target="menu"
92
+ popover="auto"
93
+ class="bg-white border rounded-md shadow-md"
94
+ >
95
+ <div class="p-2">
96
+ <button class="block i-full text-start p-2 hover:bg-shade">Copy</button>
97
+ <button class="block i-full text-start p-2 hover:bg-shade">Paste</button>
98
+ <button class="block i-full text-start p-2 hover:bg-shade">Delete</button>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Index Controller Test -->
106
+ <div class="card">
107
+ <div class="p-4">
108
+ <h2 class="text-lg font-semibold mb-3">Index Controller Test (Turbo Frame)</h2>
109
+ <p class="text-subtle mb-4">Testing CSP-safe DOM manipulation for Turbo Frame updates:</p>
110
+
111
+ <turbo-frame id="csp-test-frame">
112
+ <div class="bg-shade border border-main rounded-md p-4">
113
+ <p class="mb-2">This content can be updated via Turbo Frame requests using CSP-safe DOM methods.</p>
114
+ <p class="text-subtle">No innerHTML usage - only DOM parser and appendChild methods.</p>
115
+ </div>
116
+ </turbo-frame>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Asset Loading Test -->
121
+ <div class="card">
122
+ <div class="p-4">
123
+ <h2 class="text-lg font-semibold mb-3">Asset Loading Test</h2>
124
+ <p class="text-subtle mb-4">Verifying all Rails Pulse assets load correctly under strict CSP:</p>
125
+
126
+ <div class="flex flex-col gap">
127
+ <div class="flex justify-between items-center">
128
+ <span>CSS Bundle (rails-pulse.css):</span>
129
+ <span id="css-status" class="badge badge--secondary">Loading...</span>
130
+ </div>
131
+
132
+ <div class="flex justify-between items-center">
133
+ <span>JavaScript Bundle (rails-pulse.js):</span>
134
+ <span id="js-status" class="badge badge--secondary">Loading...</span>
135
+ </div>
136
+
137
+ <div class="flex justify-between items-center">
138
+ <span>Icons Bundle (rails-pulse-icons.js):</span>
139
+ <span id="icons-status" class="badge badge--secondary">Loading...</span>
140
+ </div>
141
+
142
+ <div class="flex justify-between items-center">
143
+ <span>Stimulus Controllers:</span>
144
+ <span id="stimulus-status" class="badge badge--secondary">Loading...</span>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- AJAX Request Test -->
151
+ <div class="card">
152
+ <div class="p-4">
153
+ <h2 class="text-lg font-semibold mb-3">AJAX Request Test</h2>
154
+ <p class="text-subtle mb-4">Testing CSP compliance for dynamic content loading:</p>
155
+
156
+ <button
157
+ id="ajax-test-btn"
158
+ class="btn btn--primary mb-3"
159
+ data-testid="ajax-test-button"
160
+ >
161
+ Test AJAX Loading
162
+ </button>
163
+
164
+ <div id="ajax-result" class="bg-shade border border-main rounded-md p-4">
165
+ <span class="text-subtle">Click button to test AJAX request...</span>
166
+ </div>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Chart Component Test -->
171
+ <div class="card">
172
+ <div class="p-4">
173
+ <h2 class="text-lg font-semibold mb-3">Chart Component Test</h2>
174
+ <p class="text-subtle mb-4">Testing rails_charts CSP compliance:</p>
175
+
176
+ <div id="chart-container" class="bg-shade border border-main rounded-md p-4 text-center">
177
+ <span class="text-subtle">Chart components would render here in a real dashboard</span>
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ <!-- CSP Violation Test -->
183
+ <div class="card">
184
+ <div class="p-4">
185
+ <h2 class="text-lg font-semibold mb-3">CSP Violation Summary</h2>
186
+ <p class="mb-2">Check the browser console for any CSP violations.</p>
187
+ <p class="text-subtle mb-4">A properly implemented CSP-safe system should show zero violations.</p>
188
+
189
+ <div class="bg-shade border border-main rounded-md p-4">
190
+ <div class="mb-2">
191
+ <strong class="font-semibold">Expected Result:</strong>
192
+ <span class="text-subtle">No CSP violations in browser console</span>
193
+ </div>
194
+ <div>
195
+ <strong class="font-semibold">Violation Count:</strong>
196
+ <span id="violation-count" class="badge badge--secondary">0</span>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+
202
+ </div>
203
+ </div>
204
+
205
+ <%= content_for :head do %>
206
+ <script src="/rails-pulse-assets/csp-test.js" nonce="<%= request_nonce %>"></script>
207
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= bar_chart @component_data, height: "100%", options: bar_chart_options(units: "ms") %>
@@ -0,0 +1,64 @@
1
+ <div class="row">
2
+ <%= cached_component(component: "metric_card", id: "average_response_times", class: "grid-item block") %>
3
+ <%= cached_component(component: "metric_card", id: "percentile_response_times", class: "grid-item block") %>
4
+ <%= cached_component(component: "metric_card", id: "request_count_totals", class: "grid-item block") %>
5
+ <%= cached_component(component: "metric_card", id: "error_rate_per_route", class: "grid-item block") %>
6
+ </div>
7
+
8
+ <div class="row">
9
+ <div class="grid-item" style="height:300px">
10
+ <%= cached_component(
11
+ component: "panel",
12
+ id: "dashboard_average_response_time",
13
+ card_classes: 'b-full',
14
+ title: 'Average Response Time',
15
+ help_heading: 'Average Response Time',
16
+ help_text: 'This panel measures the average server-side response time in milliseconds for all HTTP requests processed by your application. This is the time it takes from the server receiving a request until the request is returned to the client in milliseconds.',
17
+ actions: [{ url: routes_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }],
18
+ refresh_action: true,
19
+ content_partial: 'rails_pulse/dashboard/charts/bar_chart')
20
+ %>
21
+ </div>
22
+
23
+ <div class="grid-item">
24
+ <%= cached_component(
25
+ component: "panel",
26
+ id: "dashboard_p95_response_time",
27
+ card_classes: 'b-full',
28
+ title: 'Query Performance',
29
+ help_heading: 'Query Performance',
30
+ help_text: 'This panel measures the 95th percentile response time in milliseconds for queries in your application. This represents the query performance below which 95% of queries fall, indicating the upper-bound performance for most database operations.',
31
+ actions: [{ url: routes_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }],
32
+ refresh_action: true,
33
+ content_partial: 'rails_pulse/dashboard/charts/bar_chart')
34
+ %>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="row">
39
+ <div class="grid-item">
40
+ <%= cached_component(
41
+ component: "panel",
42
+ id: "dashboard_slow_routes",
43
+ title: 'Slowest Routes This Week',
44
+ help_heading: 'Slowest Routes',
45
+ help_text: 'This panel shows the slowest routes in your application this week compared to last week, including average response time, week-over-week change, and total request count.',
46
+ refresh_action: true,
47
+ content_partial: 'rails_pulse/dashboard/tables/standard_table',
48
+ class: "table-container")
49
+ %>
50
+ </div>
51
+
52
+ <div class="grid-item">
53
+ <%= cached_component(
54
+ component: "panel",
55
+ id: "dashboard_slow_queries",
56
+ title: 'Slowest Queries This Week',
57
+ help_heading: 'Slowest Queries',
58
+ help_text: 'This panel shows the slowest database queries in your application this week, including average execution time and when they were last seen.',
59
+ refresh_action: true,
60
+ content_partial: 'rails_pulse/dashboard/tables/standard_table',
61
+ class: "table-container")
62
+ %>
63
+ </div>
64
+ </div>
@@ -0,0 +1,32 @@
1
+ <table class="table mbs-4">
2
+ <thead>
3
+ <tr>
4
+ <th>Route</th>
5
+ <th>This Week Avg (ms)</th>
6
+ <th>Last Week Avg (ms)</th>
7
+ <th>Change</th>
8
+ <th>Requests</th>
9
+ </tr>
10
+ </thead>
11
+ <tbody>
12
+ <% if table_data.any? %>
13
+ <% table_data.each do |route_data| %>
14
+ <tr>
15
+ <td><%= route_data[:route_path] %></td>
16
+ <td class="<%= route_data[:trend] == 'worse' ? 'highlight-red' : (route_data[:trend] == 'better' ? 'highlight-green' : '') %>">
17
+ <%= route_data[:this_week_avg] %>
18
+ </td>
19
+ <td><%= route_data[:last_week_avg] %></td>
20
+ <td class="<%= route_data[:percentage_change] > 5 ? 'highlight-red' : (route_data[:percentage_change] < -5 ? 'highlight-green' : '') %>">
21
+ <%= route_data[:percentage_change] > 0 ? '+' : '' %><%= route_data[:percentage_change] %>%
22
+ </td>
23
+ <td><%= route_data[:request_count] %></td>
24
+ </tr>
25
+ <% end %>
26
+ <% else %>
27
+ <tr>
28
+ <td colspan="5" class="text-center text-subtle">No route data available for the selected time period</td>
29
+ </tr>
30
+ <% end %>
31
+ </tbody>
32
+ </table>
@@ -0,0 +1 @@
1
+ <%= render 'rails_pulse/components/table', table_data: @component_data %>
@@ -0,0 +1,43 @@
1
+ <% if operation.label&.match?(/(\w+)#(\w+)/) %>
2
+ <% controller, action = operation.label.match(/(\w+)#(\w+)/)&.captures %>
3
+ <dt>Controller</dt>
4
+ <dd><%= html_escape(controller) %></dd>
5
+ <dt>Action</dt>
6
+ <dd><%= html_escape(action) %></dd>
7
+ <% end %>
8
+
9
+ <% total_db_time = operation.request.operations
10
+ .where(operation_type: ["sql"])
11
+ .sum(:duration) %>
12
+ <% total_view_time = operation.request.operations
13
+ .where(operation_type: ["template", "partial", "layout", "collection"])
14
+ .sum(:duration) %>
15
+ <% pure_controller_time = operation.duration - total_db_time - total_view_time %>
16
+
17
+ <% if pure_controller_time > 0 %>
18
+ <dt>Pure Logic Time</dt>
19
+ <dd>
20
+ <%= pure_controller_time.round(1) %>ms
21
+ <% if pure_controller_time > 100 %>
22
+ - consider optimization
23
+ <% end %>
24
+ </dd>
25
+ <% end %>
26
+
27
+ <% db_operations_count = operation.request.operations
28
+ .where(operation_type: ["sql"])
29
+ .count %>
30
+ <% if db_operations_count > 0 %>
31
+ <dt>Database Queries</dt>
32
+ <dd>
33
+ <%= db_operations_count %>
34
+ <% if db_operations_count > 10 %>
35
+ - potential N+1 queries
36
+ <% end %>
37
+ </dd>
38
+ <% end %>
39
+
40
+ <% if operation.codebase_location.present? %>
41
+ <dt>Source</dt>
42
+ <dd><code><%= html_escape(operation.codebase_location.split('/').last(3).join('/')) %></code></dd>
43
+ <% end %>
@@ -0,0 +1,12 @@
1
+ <dt>SQL Analysis</dt>
2
+ <dd>
3
+ <% if operation.duration > RailsPulse.configuration.query_thresholds[:critical] %>
4
+ Critically slow query (><%= RailsPulse.configuration.query_thresholds[:critical] %> ms)
5
+ <% elsif operation.duration > RailsPulse.configuration.query_thresholds[:very_slow] %>
6
+ Very slow query (><%= RailsPulse.configuration.query_thresholds[:very_slow] %> ms)
7
+ <% elsif operation.duration > RailsPulse.configuration.query_thresholds[:slow] %>
8
+ Slow query (><%= RailsPulse.configuration.query_thresholds[:slow] %> ms)
9
+ <% else %>
10
+ Normal query (<<%= RailsPulse.configuration.query_thresholds[:slow] %> ms)
11
+ <% end %>
12
+ </dd>
@@ -0,0 +1,15 @@
1
+ <dt>Duration</dt>
2
+ <dd><%= operation.duration.round(2) %>ms</dd>
3
+
4
+ <dt>Operation Type</dt>
5
+ <dd><%= operation.operation_type.humanize %></dd>
6
+
7
+ <% if operation.label.present? && operation.label != operation.operation_type %>
8
+ <dt>Operation</dt>
9
+ <dd><%= html_escape(truncate(operation.label, length: 150)) %></dd>
10
+ <% end %>
11
+
12
+ <% if operation.codebase_location.present? %>
13
+ <dt>Source</dt>
14
+ <dd><code><%= html_escape(operation.codebase_location.split('/').last(3).join('/')) %></code></dd>
15
+ <% end %>
@@ -0,0 +1,69 @@
1
+ <dt>Operation Performance</dt>
2
+ <dd>
3
+ <% case operation.operation_type %>
4
+ <% when "cache_read", "cache_write" %>
5
+ <% if operation.operation_type == "cache_read" %>
6
+ <% if operation.duration > 10 %>
7
+ Slow cache read (>10ms)
8
+ <% else %>
9
+ Fast cache read
10
+ <% end %>
11
+ <% else %>
12
+ <% if operation.duration > 50 %>
13
+ Slow cache write (>50ms)
14
+ <% else %>
15
+ Fast cache write
16
+ <% end %>
17
+ <% end %>
18
+ <% when "http" %>
19
+ <% if operation.duration > 1000 %>
20
+ Very slow request (>1s)
21
+ <% elsif operation.duration > 500 %>
22
+ Slow request (>500ms)
23
+ <% else %>
24
+ Fast request
25
+ <% end %>
26
+ <% when "job" %>
27
+ <% if operation.duration > 5000 %>
28
+ Long-running job (>5s)
29
+ <% elsif operation.duration > 1000 %>
30
+ Moderate job (>1s)
31
+ <% else %>
32
+ Quick job
33
+ <% end %>
34
+ <% when "mailer" %>
35
+ <% if operation.duration > 2000 %>
36
+ Slow email delivery (>2s)
37
+ <% elsif operation.duration > 500 %>
38
+ Moderate delivery (>500ms)
39
+ <% else %>
40
+ Fast delivery
41
+ <% end %>
42
+ <% when "storage" %>
43
+ <% if operation.duration > 1000 %>
44
+ Slow storage operation (>1s)
45
+ <% elsif operation.duration > 500 %>
46
+ Moderate storage (>500ms)
47
+ <% else %>
48
+ Fast storage
49
+ <% end %>
50
+ <% else %>
51
+ Duration: <%= operation.duration.round(2) %>ms
52
+ <% end %>
53
+ </dd>
54
+
55
+ <% if operation.operation_type == "http" && operation.label&.match?(/https?:\/\/([^\/\s]+)/) %>
56
+ <% domain = operation.label.match(/https?:\/\/([^\/\s]+)/)&.captures&.first %>
57
+ <dt>External Service</dt>
58
+ <dd><%= html_escape(domain) %></dd>
59
+ <% end %>
60
+
61
+ <% if operation.label.present? && operation.label != operation.operation_type %>
62
+ <dt>Details</dt>
63
+ <dd><%= html_escape(truncate(operation.label, length: 100)) %></dd>
64
+ <% end %>
65
+
66
+ <% if operation.codebase_location.present? %>
67
+ <dt>Source</dt>
68
+ <dd><code><%= html_escape(operation.codebase_location.split('/').last(3).join('/')) %></code></dd>
69
+ <% end %>
@@ -0,0 +1,39 @@
1
+ <dt>Code Location</dt>
2
+ <dd>
3
+ <code>
4
+ <%= html_escape(operation.codebase_location.present? ? operation.codebase_location.split('/').last(3).join('/') : 'N/A') %>
5
+ </code>
6
+ </dd>
7
+
8
+ <dt>View Performance</dt>
9
+ <dd>
10
+ <% if operation.duration > 100 %>
11
+ Slow render (>100ms)
12
+ <% elsif operation.duration > 50 %>
13
+ Moderate render (>50ms)
14
+ <% else %>
15
+ Fast render
16
+ <% end %>
17
+ </dd>
18
+
19
+ <% if operation.label&.include?('_') %>
20
+ <dt>Template Type</dt>
21
+ <dd>Partial template - ensure data is pre-loaded in controller</dd>
22
+ <% end %>
23
+
24
+ <% view_start = operation.occurred_at %>
25
+ <% view_end = operation.occurred_at + operation.duration %>
26
+ <% concurrent_db_ops = operation.request.operations
27
+ .where(operation_type: ["sql"])
28
+ .where("occurred_at >= ? AND occurred_at <= ?", view_start, view_end)
29
+ .count %>
30
+
31
+ <% if concurrent_db_ops > 0 %>
32
+ <dt>Queries During Rendering</dt>
33
+ <dd><%= concurrent_db_ops %> database <%= 'query'.pluralize(concurrent_db_ops) %></dd>
34
+ <% end %>
35
+
36
+ <% if operation.duration > 50 %>
37
+ <dt>Optimization Suggestion</dt>
38
+ <dd>Consider fragment caching: <code>&lt;% cache do %&gt;</code></dd>
39
+ <% end %>
@@ -0,0 +1,79 @@
1
+ <%= render 'rails_pulse/components/breadcrumbs' %>
2
+
3
+ <h1>Operation <%= @operation.id %></h1>
4
+
5
+ <div class="card">
6
+ <%= turbo_frame_tag "operation_#{@operation.id}_details" do %>
7
+ <dl class="descriptive-list">
8
+ <dt>
9
+ <% if @operation.operation_type == "sql" %>
10
+ Query
11
+ <% elsif ["template", "partial", "layout", "collection"].include?(@operation.operation_type) %>
12
+ View Render
13
+ <% else %>
14
+ <%= html_escape(@operation.label) %>
15
+ <% end %>
16
+ </dt>
17
+ <dd>
18
+ <% if @operation.operation_type == "sql" %>
19
+ <%= render 'rails_pulse/components/code_panel', { value: @operation.label } %>
20
+ <% else %>
21
+ <%= html_escape(@operation.label) %>
22
+ <% end %>
23
+ </dd>
24
+
25
+ <dt>Operation Type</dt>
26
+ <dd><pre><%= @operation.operation_type %></pre></dd>
27
+
28
+ <dt>Duration</dt>
29
+ <dd>
30
+ <%= @operation.duration.round(2) %> ms
31
+ </dd>
32
+
33
+ <dt>Request Impact</dt>
34
+ <dd>
35
+ <% impact = (@operation.duration / @request.duration * 100).round(1) %>
36
+ <%= impact %>% of total request time
37
+ </dd>
38
+
39
+ <dt>Occurred At</dt>
40
+ <dd><%= human_readable_occurred_at @operation.occurred_at %></dd>
41
+
42
+ <% begin %>
43
+ <%= render partial: "operation_analysis_#{categorize_operation(@operation.operation_type)}", locals: { operation: @operation } %>
44
+ <% rescue ActionView::MissingTemplate %>
45
+ <%= render partial: "operation_analysis_generic", locals: { operation: @operation } %>
46
+ <% end %>
47
+
48
+ <% if @performance_context.any? %>
49
+ <dt>Performance Comparison</dt>
50
+ <dd>
51
+ Average: <%= @performance_context[:average].round(1) %>ms<br>
52
+ 95th Percentile: <%= @performance_context[:percentile_95].round(1) %>ms<br>
53
+ Based on <%= @performance_context[:count] %> recent operations
54
+ </dd>
55
+ <% end %>
56
+
57
+ <% if @related_operations.any? %>
58
+ <dt>Related Operations</dt>
59
+ <dd>
60
+ <%= pluralize(@related_operations.count, 'similar operation') %> in this request
61
+ <% if @related_operations.count > 2 %>
62
+ <br>Potential N+1 query pattern detected
63
+ <% end %>
64
+ </dd>
65
+ <% end %>
66
+
67
+ <% if @optimization_suggestions.any? %>
68
+ <dt>Optimization Tips</dt>
69
+ <dd>
70
+ <% @optimization_suggestions.each do |suggestion| %>
71
+ <div>
72
+ <%= suggestion[:title] %> - <%= suggestion[:description] %>
73
+ </div>
74
+ <% end %>
75
+ </dd>
76
+ <% end %>
77
+ </dl>
78
+ <% end %>
79
+ </div>
@@ -0,0 +1,19 @@
1
+ <% columns = [
2
+ { field: :occurred_at, label: 'Occurred At', class: 'w-auto' },
3
+ { field: :duration, label: 'Duration', class: 'w-32'}
4
+ ] %>
5
+
6
+ <table class="table mbs-4">
7
+ <%= render "rails_pulse/components/table_head", columns: columns %>
8
+
9
+ <tbody>
10
+ <% @table_data.each do |query| %>
11
+ <tr>
12
+ <td class="whitespace-nowrap"><%= human_readable_occurred_at(query.occurred_at) %></td>
13
+ <td class="whitespace-nowrap"><%= query.duration.round(2) %> ms</td>
14
+ </tr>
15
+ <% end %>
16
+ </tbody>
17
+ </table>
18
+
19
+ <%= render "rails_pulse/components/table_pagination" %>
@@ -0,0 +1,31 @@
1
+ <% columns = [
2
+ { field: :normalized_sql, label: 'Query', class: 'w-auto' },
3
+ { field: :execution_count, label: 'Executions', class: 'w-24' },
4
+ { field: :average_query_time_ms, label: 'Avg Time', class: 'w-24' },
5
+ { field: :total_time_consumed, label: 'Total Time', class: 'w-28' },
6
+ { field: :performance_status, label: 'Status', class: 'w-16' },
7
+ { field: :occurred_at, label: 'Last Seen', class: 'w-32' }
8
+ ] %>
9
+
10
+ <table class="table mbs-4">
11
+ <%= render "rails_pulse/components/table_head", columns: columns %>
12
+
13
+ <tbody>
14
+ <% @table_data.each do |query| %>
15
+ <tr>
16
+ <td class="whitespace-nowrap">
17
+ <div>
18
+ <%= link_to html_escape(truncate_sql(query.normalized_sql)), query_path(query), data: { turbo_frame: '_top', } %>
19
+ </div>
20
+ </td>
21
+ <td class="whitespace-nowrap"><%= number_with_delimiter query.execution_count %></td>
22
+ <td class="whitespace-nowrap"><%= query.average_query_time_ms.to_i %> ms</td>
23
+ <td class="whitespace-nowrap"><%= number_with_delimiter query.total_time_consumed.to_i %> ms</td>
24
+ <td class="whitespace-nowrap text-center"><%= query_status_indicator(query.average_query_time_ms) %></td>
25
+ <td class="whitespace-nowrap"><%= human_readable_occurred_at(query.occurred_at) if query.occurred_at %></td>
26
+ </tr>
27
+ <% end %>
28
+ </tbody>
29
+ </table>
30
+
31
+ <%= render "rails_pulse/components/table_pagination" %>