rails_pulse 0.1.3 → 0.2.2

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +134 -16
  3. data/Rakefile +315 -83
  4. data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
  5. data/app/assets/stylesheets/rails_pulse/components/datepicker.css +191 -0
  6. data/app/assets/stylesheets/rails_pulse/components/switch.css +36 -0
  7. data/app/assets/stylesheets/rails_pulse/components/tags.css +98 -0
  8. data/app/assets/stylesheets/rails_pulse/components/utilities.css +26 -0
  9. data/app/controllers/concerns/response_range_concern.rb +15 -2
  10. data/app/controllers/concerns/tag_filter_concern.rb +26 -0
  11. data/app/controllers/concerns/time_range_concern.rb +27 -8
  12. data/app/controllers/rails_pulse/application_controller.rb +73 -0
  13. data/app/controllers/rails_pulse/queries_controller.rb +18 -21
  14. data/app/controllers/rails_pulse/requests_controller.rb +80 -35
  15. data/app/controllers/rails_pulse/routes_controller.rb +4 -2
  16. data/app/controllers/rails_pulse/tags_controller.rb +51 -0
  17. data/app/helpers/rails_pulse/application_helper.rb +2 -0
  18. data/app/helpers/rails_pulse/breadcrumbs_helper.rb +1 -1
  19. data/app/helpers/rails_pulse/chart_helper.rb +1 -1
  20. data/app/helpers/rails_pulse/form_helper.rb +75 -0
  21. data/app/helpers/rails_pulse/formatting_helper.rb +21 -2
  22. data/app/helpers/rails_pulse/tags_helper.rb +29 -0
  23. data/app/javascript/rails_pulse/application.js +6 -0
  24. data/app/javascript/rails_pulse/controllers/custom_range_controller.js +115 -0
  25. data/app/javascript/rails_pulse/controllers/datepicker_controller.js +48 -0
  26. data/app/javascript/rails_pulse/controllers/global_filters_controller.js +110 -0
  27. data/app/javascript/rails_pulse/controllers/index_controller.js +11 -3
  28. data/app/models/concerns/taggable.rb +61 -0
  29. data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +1 -1
  30. data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
  31. data/app/models/rails_pulse/queries/cards/average_query_times.rb +1 -1
  32. data/app/models/rails_pulse/queries/cards/execution_rate.rb +56 -17
  33. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
  34. data/app/models/rails_pulse/queries/charts/average_query_times.rb +3 -7
  35. data/app/models/rails_pulse/queries/tables/index.rb +10 -2
  36. data/app/models/rails_pulse/query.rb +2 -0
  37. data/app/models/rails_pulse/request.rb +10 -2
  38. data/app/models/rails_pulse/requests/charts/average_response_times.rb +2 -2
  39. data/app/models/rails_pulse/requests/tables/index.rb +77 -0
  40. data/app/models/rails_pulse/route.rb +2 -0
  41. data/app/models/rails_pulse/routes/cards/average_response_times.rb +1 -1
  42. data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +1 -1
  43. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +1 -1
  44. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +16 -5
  45. data/app/models/rails_pulse/routes/tables/index.rb +14 -4
  46. data/app/models/rails_pulse/summary.rb +7 -7
  47. data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +11 -3
  48. data/app/services/rails_pulse/summary_service.rb +2 -0
  49. data/app/views/layouts/rails_pulse/_global_filters.html.erb +84 -0
  50. data/app/views/layouts/rails_pulse/_menu_items.html.erb +5 -5
  51. data/app/views/layouts/rails_pulse/application.html.erb +8 -5
  52. data/app/views/rails_pulse/components/_metric_card.html.erb +2 -2
  53. data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
  54. data/app/views/rails_pulse/components/_page_header.html.erb +20 -0
  55. data/app/views/rails_pulse/components/_sparkline_stats.html.erb +1 -1
  56. data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
  57. data/app/views/rails_pulse/operations/show.html.erb +1 -1
  58. data/app/views/rails_pulse/queries/_analysis_results.html.erb +53 -23
  59. data/app/views/rails_pulse/queries/_show_table.html.erb +33 -5
  60. data/app/views/rails_pulse/queries/_table.html.erb +4 -6
  61. data/app/views/rails_pulse/queries/index.html.erb +3 -7
  62. data/app/views/rails_pulse/queries/show.html.erb +3 -7
  63. data/app/views/rails_pulse/requests/_table.html.erb +32 -19
  64. data/app/views/rails_pulse/requests/index.html.erb +45 -55
  65. data/app/views/rails_pulse/requests/show.html.erb +1 -3
  66. data/app/views/rails_pulse/routes/_requests_table.html.erb +41 -0
  67. data/app/views/rails_pulse/routes/_table.html.erb +4 -8
  68. data/app/views/rails_pulse/routes/index.html.erb +4 -8
  69. data/app/views/rails_pulse/routes/show.html.erb +6 -12
  70. data/app/views/rails_pulse/tags/_tag_manager.html.erb +73 -0
  71. data/config/initializers/rails_charts_csp_patch.rb +32 -40
  72. data/config/routes.rb +5 -0
  73. data/db/migrate/20250930105043_install_rails_pulse_tables.rb +23 -0
  74. data/db/rails_pulse_schema.rb +4 -1
  75. data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +25 -9
  76. data/lib/generators/rails_pulse/install_generator.rb +30 -7
  77. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +75 -2
  78. data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +3 -2
  79. data/lib/generators/rails_pulse/templates/rails_pulse.rb +21 -0
  80. data/lib/generators/rails_pulse/upgrade_generator.rb +147 -30
  81. data/lib/rails_pulse/configuration.rb +16 -1
  82. data/lib/rails_pulse/engine.rb +21 -0
  83. data/lib/rails_pulse/version.rb +1 -1
  84. data/public/rails-pulse-assets/rails-pulse-icons.js +16 -15
  85. data/public/rails-pulse-assets/rails-pulse-icons.js.map +1 -1
  86. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  87. data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
  88. data/public/rails-pulse-assets/rails-pulse.js +73 -69
  89. data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
  90. metadata +20 -5
  91. data/app/views/rails_pulse/components/_breadcrumbs.html.erb +0 -12
  92. data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +0 -54
  93. data/db/migrate/20250916031656_add_analysis_to_rails_pulse_queries.rb +0 -13
@@ -2,24 +2,32 @@ module RailsPulse
2
2
  module Routes
3
3
  module Tables
4
4
  class Index
5
- def initialize(ransack_query:, period_type: nil, start_time:, params:)
5
+ def initialize(ransack_query:, period_type: nil, start_time:, params:, disabled_tags: [])
6
6
  @ransack_query = ransack_query
7
7
  @period_type = period_type
8
8
  @start_time = start_time
9
9
  @params = params
10
+ @disabled_tags = disabled_tags
10
11
  end
11
12
 
12
13
  def to_table
13
14
  # Check if we have explicit ransack sorts
14
15
  has_sorts = @ransack_query.sorts.any?
15
16
 
16
- base_query = @ransack_query.result(distinct: false)
17
+ # Store sorts for later and get result without ordering
18
+ # This prevents PostgreSQL GROUP BY issues with ORDER BY columns
19
+ base_query = @ransack_query.result(distinct: false).reorder(nil)
17
20
  .joins("INNER JOIN rails_pulse_routes ON rails_pulse_routes.id = rails_pulse_summaries.summarizable_id")
18
21
  .where(
19
22
  summarizable_type: "RailsPulse::Route",
20
23
  period_type: @period_type
21
24
  )
22
25
 
26
+ # Apply tag filters by excluding routes with disabled tags
27
+ @disabled_tags.each do |tag|
28
+ base_query = base_query.where.not("rails_pulse_routes.tags LIKE ?", "%#{tag}%")
29
+ end
30
+
23
31
  base_query = base_query.where(summarizable_id: @route.id) if @route
24
32
 
25
33
  # Apply grouping and aggregation
@@ -29,7 +37,8 @@ module RailsPulse
29
37
  "rails_pulse_summaries.summarizable_type",
30
38
  "rails_pulse_routes.id",
31
39
  "rails_pulse_routes.path",
32
- "rails_pulse_routes.method"
40
+ "rails_pulse_routes.method",
41
+ "rails_pulse_routes.tags"
33
42
  )
34
43
  .select(
35
44
  "rails_pulse_summaries.summarizable_id",
@@ -37,6 +46,7 @@ module RailsPulse
37
46
  "rails_pulse_routes.id as route_id",
38
47
  "rails_pulse_routes.path",
39
48
  "rails_pulse_routes.method as route_method",
49
+ "rails_pulse_routes.tags",
40
50
  "AVG(rails_pulse_summaries.avg_duration) as avg_duration",
41
51
  "MAX(rails_pulse_summaries.max_duration) as max_duration",
42
52
  "SUM(rails_pulse_summaries.count) as count",
@@ -55,7 +65,7 @@ module RailsPulse
55
65
  grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").send(direction))
56
66
  when "max_duration_sort"
57
67
  grouped_query = grouped_query.order(Arel.sql("MAX(rails_pulse_summaries.max_duration)").send(direction))
58
- when "count_sort"
68
+ when "count_sort", "request_count_sort"
59
69
  grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count)").send(direction))
60
70
  when "requests_per_minute"
61
71
  grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count) / 60.0").send(direction))
@@ -36,9 +36,10 @@ module RailsPulse
36
36
  # Ransack configuration
37
37
  def self.ransackable_attributes(auth_object = nil)
38
38
  %w[
39
- period_start period_end avg_duration max_duration count error_count
39
+ period_start period_end avg_duration min_duration max_duration count error_count
40
40
  requests_per_minute error_rate_percentage route_path_cont
41
41
  execution_count total_time_consumed normalized_sql
42
+ summarizable_id summarizable_type
42
43
  ]
43
44
  end
44
45
 
@@ -46,17 +47,16 @@ module RailsPulse
46
47
  %w[route query]
47
48
  end
48
49
 
49
- # Custom ransackers for calculated fields (designed to work with GROUP BY queries)
50
- ransacker :count do
51
- Arel.sql("SUM(rails_pulse_summaries.count)") # Use SUM for proper grouping
52
- end
50
+ # Note: Basic fields like count, avg_duration, min_duration, max_duration
51
+ # are handled automatically by Ransack using actual database columns
53
52
 
53
+ # Custom ransackers for calculated fields only
54
54
  ransacker :requests_per_minute do
55
- Arel.sql("SUM(rails_pulse_summaries.count) / 60.0") # Use SUM for consistency
55
+ Arel.sql("rails_pulse_summaries.count / 60.0")
56
56
  end
57
57
 
58
58
  ransacker :error_rate_percentage do
59
- Arel.sql("(SUM(rails_pulse_summaries.error_count) * 100.0) / SUM(rails_pulse_summaries.count)") # Use SUM for both
59
+ Arel.sql("(rails_pulse_summaries.error_count * 100.0) / rails_pulse_summaries.count")
60
60
  end
61
61
 
62
62
 
@@ -38,9 +38,17 @@ module RailsPulse
38
38
 
39
39
  def count_tables
40
40
  tables = []
41
- tables.concat(sql.scan(/FROM\s+(\w+)/i).flatten)
42
- tables.concat(sql.scan(/JOIN\s+(\w+)/i).flatten)
43
- tables.uniq.length
41
+
42
+ # Match FROM clause with various table name formats
43
+ # Handles: table_name, schema.table, "quoted_table", `backtick_table`
44
+ tables.concat(sql.scan(/FROM\s+(?:`([^`]+)`|"([^"]+)"|'([^']+)'|(\w+(?:\.\w+)?))/i).flatten.compact)
45
+
46
+ # Match JOIN clauses (INNER JOIN, LEFT JOIN, etc.)
47
+ tables.concat(sql.scan(/(?:INNER\s+|LEFT\s+|RIGHT\s+|FULL\s+|CROSS\s+)?JOIN\s+(?:`([^`]+)`|"([^"]+)"|'([^']+)'|(\w+(?:\.\w+)?))/i).flatten.compact)
48
+
49
+ # Remove schema prefixes for uniqueness check (schema.table -> table)
50
+ normalized_tables = tables.map { |table| table.split(".").last }
51
+ normalized_tables.uniq.length
44
52
  end
45
53
 
46
54
  def count_joins
@@ -74,6 +74,7 @@ module RailsPulse
74
74
  route_groups = Request
75
75
  .where(occurred_at: start_time...end_time)
76
76
  .where.not(route_id: nil)
77
+ .joins(:route)
77
78
  .group(:route_id)
78
79
 
79
80
  # Calculate basic aggregates
@@ -132,6 +133,7 @@ module RailsPulse
132
133
  query_groups = Operation
133
134
  .where(occurred_at: start_time...end_time)
134
135
  .where.not(query_id: nil)
136
+ .joins(:query)
135
137
  .group(:query_id)
136
138
 
137
139
  basic_stats = query_groups.pluck(
@@ -0,0 +1,84 @@
1
+ <% global_filters = session_global_filters %>
2
+ <% has_global_filters = global_filters['start_time'].present? && global_filters['end_time'].present? %>
3
+ <% has_performance_filter = global_filters['performance_threshold'].present? %>
4
+ <% disabled_tags = global_filters['disabled_tags'] || [] %>
5
+ <% has_tag_filters = disabled_tags.any? %>
6
+ <% has_any_filters = has_global_filters || has_performance_filter %>
7
+ <% current_date_range = has_global_filters ? "#{global_filters['start_time']} to #{global_filters['end_time']}" : "" %>
8
+ <% current_threshold = global_filters['performance_threshold'] %>
9
+
10
+ <div data-controller="rails-pulse--global-filters" data-rails-pulse--global-filters-active-value="<%= has_any_filters %>">
11
+ <%= link_to '#', "aria-label": "Global filters", role: "button", data: { action: "rails-pulse--global-filters#open", "rails-pulse--global-filters-target": "indicator" } do %>
12
+ <%= rails_pulse_icon has_any_filters ? 'list-filter-plus' : 'list-filter', width: '20' %>
13
+ <% end %>
14
+
15
+ <div data-rails-pulse--global-filters-target="wrapper" data-action="click->rails-pulse--global-filters#closeOnClickOutside" style="display: none; position: fixed; inset: 0; background-color: rgba(0, 0, 0, 0.8); z-index: 1000; align-items: center; justify-content: center;">
16
+ <div class="dialog" data-rails-pulse--global-filters-target="dialog" style="position: relative; opacity: 1; transform: scale(1);">
17
+ <div class="dialog__content">
18
+ <h2 class="text-lg font-semibold mb-4">Global Filters</h2>
19
+ <p class="text-sm text-subtle mb-4">Set default time filters that persist across all pages. These can be overridden by page-specific filters.</p>
20
+
21
+ <%= form_with url: settings_global_filters_path, method: :patch, local: true, data: { action: "submit->rails-pulse--global-filters#submit" } do |form| %>
22
+ <div class="flex flex-col gap mb-4">
23
+ <div class="flex flex-col gap" style="--row-gap: 0.5rem">
24
+ <label for="global_date_range" class="text-sm font-medium">Date Range</label>
25
+ <input
26
+ type="text"
27
+ name="date_range"
28
+ id="global_date_range"
29
+ value="<%= current_date_range %>"
30
+ placeholder="Pick date range"
31
+ class="input"
32
+ data-controller="rails-pulse--datepicker"
33
+ data-rails-pulse--datepicker-mode-value="range"
34
+ data-rails-pulse--datepicker-show-months-value="2"
35
+ data-rails-pulse--datepicker-type-value="datetime"
36
+ data-rails-pulse--global-filters-target="dateRange"
37
+ />
38
+ </div>
39
+
40
+ <div class="flex flex-col gap" style="--row-gap: 0.5rem">
41
+ <label for="global_performance_threshold" class="text-sm font-medium">Performance Threshold</label>
42
+ <select name="performance_threshold" id="global_performance_threshold" class="input">
43
+ <option value="">All Requests</option>
44
+ <option value="slow" <%= 'selected' if current_threshold == 'slow' %>>Slow and Above</option>
45
+ <option value="very_slow" <%= 'selected' if current_threshold == 'very_slow' %>>Very Slow and Above</option>
46
+ <option value="critical" <%= 'selected' if current_threshold == 'critical' %>>Critical Only</option>
47
+ </select>
48
+ <p class="text-xs text-subtle">Filter requests/queries by performance threshold</p>
49
+ </div>
50
+
51
+ <div class="flex flex-col gap" style="--row-gap: 0.5rem">
52
+ <label class="text-sm font-medium">Tag Visibility</label>
53
+ <p class="text-xs text-subtle">Toggle visibility of tagged data. Unchecked tags will be hidden from all views.</p>
54
+ <div class="flex flex-col gap" style="--row-gap: 0.75rem">
55
+ <% tags = RailsPulse.configuration.tags + ["non_tagged"] %>
56
+ <% tags.each do |tag| %>
57
+ <div class="flex items-center gap">
58
+ <input
59
+ type="checkbox"
60
+ name="enabled_tags[]"
61
+ id="tag_<%= tag %>"
62
+ value="<%= tag %>"
63
+ <%= 'checked="checked"' if tag == "non_tagged" ? session[:show_non_tagged] != false : !disabled_tags.include?(tag) %>
64
+ class="switch"
65
+ role="switch"
66
+ />
67
+ <label class="text-sm font-medium" for="tag_<%= tag %>">
68
+ <%= tag.humanize %>
69
+ </label>
70
+ </div>
71
+ <% end %>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <div class="flex items-center justify-end gap">
77
+ <%= form.button "Clear", type: "submit", name: "clear", value: "true", class: "btn btn--borderless", formnovalidate: true %>
78
+ <%= form.submit "Apply Filters", class: "btn" %>
79
+ </div>
80
+ <% end %>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
@@ -8,12 +8,12 @@
8
8
  <span class="overflow-ellipsis">Routes</span>
9
9
  <% end %>
10
10
 
11
- <%= link_to requests_path, class: 'btn sidebar-menu__button' do %>
12
- <%= rails_pulse_icon 'audio-lines', width: '16' %>
13
- <span class="overflow-ellipsis">Requests</span>
14
- <% end %>
15
-
16
11
  <%= link_to queries_path, class: 'btn sidebar-menu__button' do %>
17
12
  <%= rails_pulse_icon 'database', width: '16' %>
18
13
  <span class="overflow-ellipsis">Queries</span>
19
14
  <% end %>
15
+
16
+ <%= link_to requests_path, class: 'btn sidebar-menu__button' do %>
17
+ <%= rails_pulse_icon 'audio-lines', width: '16' %>
18
+ <span class="overflow-ellipsis">Requests</span>
19
+ <% end %>
@@ -53,10 +53,14 @@
53
53
  </nav>
54
54
  </div>
55
55
 
56
- <div class="flex items-center" data-controller="rails-pulse--color-scheme">
57
- <%= link_to '#', "aria-label": "Toggle color scheme", role: "button", data: { action: "rails-pulse--color-scheme#toggle" } do %>
58
- <%= rails_pulse_icon 'sun', width: '48' %>
59
- <% end %>
56
+ <div class="flex items-center gap" style="--column-gap: 0.5rem">
57
+ <%= render 'layouts/rails_pulse/global_filters' %>
58
+
59
+ <div data-controller="rails-pulse--color-scheme">
60
+ <%= link_to '#', "aria-label": "Toggle color scheme", role: "button", data: { action: "rails-pulse--color-scheme#toggle" } do %>
61
+ <%= rails_pulse_icon 'sun', width: '48' %>
62
+ <% end %>
63
+ </div>
60
64
  </div>
61
65
  </header>
62
66
 
@@ -65,6 +69,5 @@
65
69
  <%= yield %>
66
70
  </div>
67
71
  </main>
68
-
69
72
  </body>
70
73
  </html>
@@ -4,7 +4,7 @@
4
4
  context = data[:context]
5
5
  title = data[:title]
6
6
  summary = data[:summary]
7
- line_chart_data = data[:line_chart_data]
7
+ chart_data = data[:chart_data]
8
8
  trend_icon = data[:trend_icon]
9
9
  trend_amount = data[:trend_amount]
10
10
  trend_text = data[:trend_text]
@@ -38,7 +38,7 @@
38
38
  }
39
39
  )
40
40
  %>
41
- <%= bar_chart line_chart_data, height: "100%", options: chart_options %>
41
+ <%= bar_chart chart_data, height: "100%", options: chart_options %>
42
42
  </div>
43
43
  </div>
44
44
  </div>
@@ -26,7 +26,7 @@
26
26
  <div>
27
27
  <h4 class="text-xs font-medium text-subtle uppercase">Occurred At</h4>
28
28
  <div class="text-sm">
29
- <%= operation.occurred_at.strftime("%H:%M:%S.%L") %>
29
+ <%= operation.occurred_at.getlocal.strftime("%H:%M:%S.%L") %>
30
30
  </div>
31
31
  </div>
32
32
  <% end %>
@@ -0,0 +1,20 @@
1
+ <div class="breadcrumb-container">
2
+ <nav class="breadcrumb mis-2 mbs-2" aria-label="Breadcrumb">
3
+ <% breadcrumbs.each_with_index do |crumb, index| %>
4
+ <% if crumb[:current] %>
5
+ <span class="text-primary" aria-disabled="true" aria-current="page" role="link"><%= crumb[:title] %></span>
6
+ <% else %>
7
+ <%= link_to crumb[:title], crumb[:path] %>
8
+ <% end %>
9
+ <% unless index == breadcrumbs.length - 1 %>
10
+ <%= rails_pulse_icon 'chevron-right', width: 14, height: 14, class: "breadcrumb-separator mbe-1" %>
11
+ <% end %>
12
+ <% end %>
13
+ </nav>
14
+
15
+ <% if defined?(taggable) && taggable.present? %>
16
+ <div class="breadcrumb-tags">
17
+ <%= render 'rails_pulse/tags/tag_manager', taggable: taggable %>
18
+ </div>
19
+ <% end %>
20
+ </div>
@@ -2,7 +2,7 @@
2
2
  <h4 class="text-xl mbs-1 font-bold"><%= summary %></h4>
3
3
  </div>
4
4
  <div class="chart-container chart-container--slim">
5
- <%= bar_chart line_chart_data, height: "100%", options: sparkline_chart_options %>
5
+ <%= bar_chart chart_data, height: "100%", options: sparkline_chart_options %>
6
6
  </div>
7
7
  <div>
8
8
  <span class="badge badge--<%= trend_direction == "down" ? "positive" : "negative" %>-inverse p-0">
@@ -71,7 +71,7 @@
71
71
  <div class="grid-item">
72
72
  <%= render 'rails_pulse/components/panel', {
73
73
  title: 'Slowest Queries This Week',
74
- help_heading: 'Slowest Queries',
74
+ help_heading: 'Slowest Queries',
75
75
  help_text: 'This panel shows the slowest database queries in your application this week, including average execution time and when they were last seen.',
76
76
  actions: [{ url: queries_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }],
77
77
  card_classes: 'table-container'
@@ -1,4 +1,4 @@
1
- <%= render 'rails_pulse/components/breadcrumbs' %>
1
+ <%= render 'rails_pulse/components/page_header' %>
2
2
 
3
3
  <h1>Operation <%= @operation.id %></h1>
4
4
 
@@ -12,7 +12,37 @@
12
12
  <dd><%= query.query_stats['table_count'] || 0 %></dd>
13
13
  <dt>Joins</dt>
14
14
  <dd><%= query.query_stats['join_count'] || 0 %></dd>
15
- <dt>Complexity Score</dt>
15
+ <dt>
16
+ Complexity Score
17
+ <div data-controller="rails-pulse--popover" data-rails-pulse--popover-placement-value="bottom-start" style="display: inline-block; margin-left: 4px;">
18
+ <a href="#"
19
+ data-rails-pulse--popover-target="button"
20
+ data-action="rails-pulse--popover#toggle"
21
+ data-popovertarget="complexity-score-popover"
22
+ style="color: var(--gray-500); vertical-align: top;">
23
+ <%= rails_pulse_icon 'info', height: "14px" %>
24
+ </a>
25
+
26
+ <div popover class="popover card" data-rails-pulse--popover-target="menu" style="max-width: 22rem">
27
+ <div class="flex flex-col">
28
+ <h3 class="font-semibold leading-none mbe-2 uppercase text-sm">Complexity Score</h3>
29
+ <p class="text-sm text-subtle mbe-3">
30
+ A calculated score representing query complexity based on multiple factors:
31
+ </p>
32
+ <ul class="text-sm text-subtle" style="list-style-type: disc; padding-left: 16px; margin: 0;">
33
+ <li>Tables: +2 points per table</li>
34
+ <li>Joins: +3 points per join</li>
35
+ <li>WHERE conditions: +1 point per condition, +2 per function</li>
36
+ <li>UNIONs: +4 points each</li>
37
+ <li>Subqueries: +5 points each</li>
38
+ </ul>
39
+ <p class="text-sm text-subtle mbs-3">
40
+ Higher scores indicate more complex queries that may need optimization.
41
+ </p>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </dt>
16
46
  <dd><%= query.query_stats['estimated_complexity'] || 0 %></dd>
17
47
  <dt>Has LIMIT</dt>
18
48
  <dd><%= query.query_stats['has_limit'] ? 'Yes' : 'No' %></dd>
@@ -52,36 +82,36 @@
52
82
  </div>
53
83
  </div>
54
84
 
85
+
55
86
  <!-- Issues Summary -->
56
87
  <% if query.issues.present? && query.issues.any? %>
57
- <div class="panel panel--danger mb-4">
58
- <h3 class="text-lg bold mb-3">Issues Detected</h3>
59
- <ul>
60
- <% query.issues.each do |issue| %>
61
- <li><%= issue['description'] %></li>
62
- <% end %>
63
- </ul>
64
- </div>
88
+ <hr class="mb-4" />
89
+ <h3 class="text-lg bold">Issues Detected</h3>
90
+ <ul style="list-style-type: disc; padding-left: 20px;">
91
+ <% query.issues.each do |issue| %>
92
+ <li><%= issue['description'] %></li>
93
+ <% end %>
94
+ </ul>
65
95
  <% end %>
66
96
 
97
+
67
98
  <!-- Suggestions -->
68
99
  <% if query.suggestions.present? && query.suggestions.any? %>
69
- <div class="panel panel--info mb-4">
70
- <h3 class="text-lg bold mb-3">Optimization Suggestions</h3>
71
- <ul>
72
- <% query.suggestions.each do |suggestion| %>
73
- <li>
74
- <%= suggestion['action'] %>.
75
- <%= suggestion['benefit'] if suggestion['benefit'].present? %>
76
- </li>
77
- <% end %>
78
- </ul>
79
- </div>
100
+ <hr class="mb-4" />
101
+ <h3 class="text-lg bold">Optimization Suggestions</h3>
102
+ <ul style="list-style-type: disc; padding-left: 20px;">
103
+ <% query.suggestions.each do |suggestion| %>
104
+ <li>
105
+ <%= suggestion['action'] %>.
106
+ <%= suggestion['benefit'] if suggestion['benefit'].present? %>
107
+ </li>
108
+ <% end %>
109
+ </ul>
80
110
  <% end %>
81
111
 
82
112
  <!-- EXPLAIN Plan -->
83
113
  <% if query.explain_plan.present? %>
84
- <%= render 'rails_pulse/components/panel', title: 'Execution Plan' do %>
85
- <pre class="text-sm"><%= query.explain_plan %></pre>
86
- <% end %>
114
+ <hr class="mb-4" />
115
+ <h3 class="text-lg bold">Execution Plan</h3>
116
+ <pre class="text-sm" style="overflow: scroll"><%= query.explain_plan %></pre>
87
117
  <% end %>
@@ -1,16 +1,44 @@
1
1
  <% columns = [
2
- { field: :occurred_at, label: 'Timestamp', class: 'w-auto' },
3
- { field: :duration, label: 'Duration', class: 'w-32'}
2
+ { field: :period_start, label: 'Time Period', class: 'w-auto' },
3
+ { field: :count, label: 'Executions', class: 'w-32'},
4
+ { field: :avg_duration, label: 'Avg Duration', class: 'w-32'},
5
+ { field: :min_duration, label: 'Min Duration', class: 'w-32'},
6
+ { field: :max_duration, label: 'Max Duration', class: 'w-32'}
4
7
  ] %>
5
8
 
6
9
  <table class="table mbs-4" data-controller="rails-pulse--table-sort">
7
10
  <%= render "rails_pulse/components/table_head", columns: columns %>
8
11
 
9
12
  <tbody>
10
- <% @table_data.each do |query| %>
13
+ <% @table_data.each do |summary| %>
14
+ <%
15
+ # Determine performance class based on average duration
16
+ avg_duration_ms = summary.avg_duration&.round(2) || 0
17
+ performance_class = case avg_duration_ms
18
+ when 0..10 then "text-green-600"
19
+ when 10..50 then "text-yellow-600"
20
+ when 50..100 then "text-orange-600"
21
+ else "text-red-600"
22
+ end
23
+ %>
11
24
  <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>
25
+ <td class="whitespace-nowrap">
26
+ <%= human_readable_summary_period(summary) %>
27
+ </td>
28
+ <td class="whitespace-nowrap text-center">
29
+ <span class="font-medium"><%= summary.count %></span>
30
+ </td>
31
+ <td class="whitespace-nowrap">
32
+ <span class="<%= performance_class %> font-medium">
33
+ <%= avg_duration_ms %> ms
34
+ </span>
35
+ </td>
36
+ <td class="whitespace-nowrap text-center">
37
+ <%= summary.min_duration&.round(2) || 0 %> ms
38
+ </td>
39
+ <td class="whitespace-nowrap text-center">
40
+ <%= summary.max_duration&.round(2) || 0 %> ms
41
+ </td>
14
42
  </tr>
15
43
  <% end %>
16
44
  </tbody>
@@ -1,9 +1,8 @@
1
1
  <% columns = [
2
2
  { field: :normalized_sql, label: 'Query', class: 'w-auto' },
3
+ { field: :avg_duration_sort, label: 'Average Query Time', class: 'w-44' },
3
4
  { field: :execution_count_sort, label: 'Executions', class: 'w-24' },
4
- { field: :avg_duration_sort, label: 'Avg Time', class: 'w-24' },
5
- { field: :total_time_consumed_sort, label: 'Total Time', class: 'w-28' },
6
- { field: :performance_status, label: 'Status', class: 'w-16', sortable: false }
5
+ { field: nil, label: 'Tags', class: 'w-32' }
7
6
  ] %>
8
7
 
9
8
  <table class="table mbs-4" data-controller="rails-pulse--table-sort">
@@ -17,10 +16,9 @@
17
16
  <%= link_to html_escape(truncate_sql(summary.normalized_sql)), query_path(summary.query_id), data: { turbo_frame: '_top', } %>
18
17
  </div>
19
18
  </td>
20
- <td class="whitespace-nowrap"><%= number_with_delimiter summary.execution_count %></td>
21
19
  <td class="whitespace-nowrap"><%= summary.avg_duration.to_i %> ms</td>
22
- <td class="whitespace-nowrap"><%= number_with_delimiter summary.total_time_consumed.to_i %> ms</td>
23
- <td class="whitespace-nowrap text-center"><%= query_status_indicator(summary.avg_duration) %></td>
20
+ <td class="whitespace-nowrap"><%= number_with_delimiter summary.execution_count %></td>
21
+ <td class="whitespace-nowrap"><%= display_tag_badges(summary.tags) %></td>
24
22
  </tr>
25
23
  <% end %>
26
24
  </tbody>
@@ -1,4 +1,4 @@
1
- <%= render 'rails_pulse/components/breadcrumbs' %>
1
+ <%= render 'rails_pulse/components/page_header' %>
2
2
 
3
3
  <% unless turbo_frame_request? %>
4
4
  <div class="row">
@@ -13,13 +13,9 @@
13
13
  data-rails-pulse--index-chart-id-value="average_query_times_chart"
14
14
  >
15
15
  <%= render 'rails_pulse/components/panel', { title: 'Average Query Time', } do %>
16
- <%= search_form_for @ransack_query, url: queries_path, class: "flex items-center justify-between gap mb-4" do |form| %>
16
+ <%= search_form_for @ransack_query, url: queries_path, class: "flex items-center justify-between gap mb-4", data: { controller: "rails-pulse--custom-range" } do |form| %>
17
17
  <div class="flex items-center grow gap">
18
- <%= form.select :period_start_range,
19
- RailsPulse::QueriesController::TIME_RANGE_OPTIONS,
20
- { selected: @selected_time_range },
21
- { class: "input" }
22
- %>
18
+ <%= time_range_selector(form, time_range_options: RailsPulse::QueriesController::TIME_RANGE_OPTIONS, selected_time_range: @selected_time_range) %>
23
19
  <%= form.select :avg_duration,
24
20
  duration_options(:query),
25
21
  { selected: @selected_response_range },
@@ -1,4 +1,4 @@
1
- <%= render 'rails_pulse/components/breadcrumbs' %>
1
+ <%= render 'rails_pulse/components/page_header', taggable: @query %>
2
2
 
3
3
  <% unless turbo_frame_request? %>
4
4
  <div class="row">
@@ -13,13 +13,9 @@
13
13
  data-rails-pulse--index-chart-id-value="query_responses_chart"
14
14
  >
15
15
  <%= render 'rails_pulse/components/panel', { title: 'Query Responses' } do %>
16
- <%= search_form_for @ransack_query, url: query_path(@query), class: "flex items-center justify-between gap mb-4" do |form| %>
16
+ <%= search_form_for @ransack_query, url: query_path(@query), class: "flex items-center justify-between gap mb-4", data: { controller: "rails-pulse--custom-range" } do |form| %>
17
17
  <div class="flex items-center grow gap">
18
- <%= form.select :period_start_range,
19
- RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
20
- { selected: @selected_time_range },
21
- { class: "input" }
22
- %>
18
+ <%= time_range_selector(form, time_range_options: RailsPulse::RoutesController::TIME_RANGE_OPTIONS, selected_time_range: @selected_time_range) %>
23
19
  <%= form.select :duration,
24
20
  duration_options(:query),
25
21
  { selected: @selected_response_range },
@@ -1,28 +1,41 @@
1
- <%
2
- columns = []
3
- columns << { field: :route_path, label: 'Route', class: 'w-auto' } if @route.blank?
4
-
5
- columns += [
6
- { field: :occurred_at, label: 'Timestamp', class: 'w-36' },
7
- { field: :duration, label: 'Response Time', class: 'w-24' },
8
- { field: :status, label: 'HTTP Status', class: 'w-20' },
9
- { field: :status_indicator, label: 'Status', class: 'w-16' }
10
- ]
11
- %>
1
+ <% columns = [
2
+ { field: :occurred_at, label: 'Timestamp', class: 'w-36' },
3
+ { field: :route_path, label: 'Route', class: 'w-auto' },
4
+ { field: :duration, label: 'Response Time', class: 'w-36' },
5
+ { field: :status, label: 'Status', class: 'w-20' },
6
+ { field: nil, label: 'Tags', class: 'w-32' }
7
+ ] %>
12
8
 
13
9
  <table class="table mbs-4" data-controller="rails-pulse--table-sort">
14
10
  <%= render "rails_pulse/components/table_head", columns: columns %>
15
11
 
16
12
  <tbody>
17
- <% @table_data.each do |route_request| %>
13
+ <% @table_data.each do |request| %>
14
+ <%
15
+ # Determine performance class based on request duration
16
+ performance_class = case request.duration
17
+ when 0..100 then "text-green-600"
18
+ when 100..300 then "text-yellow-600"
19
+ when 300..1000 then "text-orange-600"
20
+ else "text-red-600"
21
+ end
22
+ %>
18
23
  <tr>
19
- <% if @route.blank? %>
20
- <td class="whitespace-nowrap"><%= link_to route_request.route.path_and_method, request_path(route_request), data: { turbo_frame: '_top' } %></td>
21
- <% end %>
22
- <td class="whitespace-nowrap"><%= link_to human_readable_occurred_at(route_request.occurred_at), request_path(route_request), data: { turbo_frame: '_top' } %></td>
23
- <td class="whitespace-nowrap"><%= route_request.duration.round(2) %> ms</td>
24
- <td class="whitespace-nowrap"><%= route_request.status %></td>
25
- <td class="whitespace-nowrap text-center"><%= request_status_indicator(route_request.duration) %></td>
24
+ <td class="whitespace-nowrap">
25
+ <%= link_to human_readable_occurred_at(request.occurred_at), request_path(request), data: { turbo_frame: '_top' } %>
26
+ </td>
27
+ <td class="whitespace-nowrap">
28
+ <%= link_to "#{request.route.path} #{request.route.method}", route_path(request.route), data: { turbo_frame: '_top' } %>
29
+ </td>
30
+ <td class="whitespace-nowrap">
31
+ <span class="<%= performance_class %> font-medium">
32
+ <%= request.duration.round(2) %> ms
33
+ </span>
34
+ </td>
35
+ <td class="whitespace-nowrap">
36
+ <span class="text-green-600"><%= request.status %></span>
37
+ </td>
38
+ <td class="whitespace-nowrap"><%= display_tag_badges(request) %></td>
26
39
  </tr>
27
40
  <% end %>
28
41
  </tbody>