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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +638 -0
- data/Rakefile +207 -0
- data/app/assets/images/rails_pulse/dashboard.png +0 -0
- data/app/assets/images/rails_pulse/menu.svg +1 -0
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/images/rails_pulse/request.png +0 -0
- data/app/assets/images/rails_pulse/routes.png +0 -0
- data/app/assets/stylesheets/rails_pulse/application.css +102 -0
- data/app/assets/stylesheets/rails_pulse/components/alert.css +24 -0
- data/app/assets/stylesheets/rails_pulse/components/badge.css +58 -0
- data/app/assets/stylesheets/rails_pulse/components/base.css +79 -0
- data/app/assets/stylesheets/rails_pulse/components/breadcrumb.css +31 -0
- data/app/assets/stylesheets/rails_pulse/components/button.css +99 -0
- data/app/assets/stylesheets/rails_pulse/components/card.css +19 -0
- data/app/assets/stylesheets/rails_pulse/components/chart.css +18 -0
- data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +86 -0
- data/app/assets/stylesheets/rails_pulse/components/descriptive_list.css +9 -0
- data/app/assets/stylesheets/rails_pulse/components/dialog.css +56 -0
- data/app/assets/stylesheets/rails_pulse/components/flash.css +47 -0
- data/app/assets/stylesheets/rails_pulse/components/input.css +80 -0
- data/app/assets/stylesheets/rails_pulse/components/layouts.css +63 -0
- data/app/assets/stylesheets/rails_pulse/components/menu.css +43 -0
- data/app/assets/stylesheets/rails_pulse/components/popover.css +36 -0
- data/app/assets/stylesheets/rails_pulse/components/prose.css +144 -0
- data/app/assets/stylesheets/rails_pulse/components/row.css +24 -0
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +79 -0
- data/app/assets/stylesheets/rails_pulse/components/skeleton.css +5 -0
- data/app/assets/stylesheets/rails_pulse/components/table.css +37 -0
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +36 -0
- data/app/controllers/concerns/chart_table_concern.rb +82 -0
- data/app/controllers/concerns/response_range_concern.rb +24 -0
- data/app/controllers/concerns/time_range_concern.rb +67 -0
- data/app/controllers/concerns/zoom_range_concern.rb +40 -0
- data/app/controllers/rails_pulse/application_controller.rb +67 -0
- data/app/controllers/rails_pulse/assets_controller.rb +33 -0
- data/app/controllers/rails_pulse/caches_controller.rb +115 -0
- data/app/controllers/rails_pulse/csp_test_controller.rb +57 -0
- data/app/controllers/rails_pulse/dashboard_controller.rb +6 -0
- data/app/controllers/rails_pulse/operations_controller.rb +219 -0
- data/app/controllers/rails_pulse/queries_controller.rb +121 -0
- data/app/controllers/rails_pulse/requests_controller.rb +69 -0
- data/app/controllers/rails_pulse/routes_controller.rb +99 -0
- data/app/helpers/rails_pulse/application_helper.rb +111 -0
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +62 -0
- data/app/helpers/rails_pulse/cached_component_helper.rb +73 -0
- data/app/helpers/rails_pulse/chart_formatters.rb +43 -0
- data/app/helpers/rails_pulse/chart_helper.rb +140 -0
- data/app/helpers/rails_pulse/formatting_helper.rb +29 -0
- data/app/helpers/rails_pulse/status_helper.rb +279 -0
- data/app/helpers/rails_pulse/table_helper.rb +54 -0
- data/app/javascript/rails_pulse/application.js +119 -0
- data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +20 -0
- data/app/javascript/rails_pulse/controllers/context_menu_controller.js +16 -0
- data/app/javascript/rails_pulse/controllers/dialog_controller.js +21 -0
- data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +67 -0
- data/app/javascript/rails_pulse/controllers/form_controller.js +39 -0
- data/app/javascript/rails_pulse/controllers/icon_controller.js +170 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +230 -0
- data/app/javascript/rails_pulse/controllers/menu_controller.js +60 -0
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +69 -0
- data/app/javascript/rails_pulse/controllers/popover_controller.js +91 -0
- data/app/javascript/rails_pulse/controllers/timezone_controller.js +106 -0
- data/app/javascript/rails_pulse/theme.js +416 -0
- data/app/jobs/rails_pulse/application_job.rb +4 -0
- data/app/jobs/rails_pulse/cleanup_job.rb +21 -0
- data/app/mailers/rails_pulse/application_mailer.rb +6 -0
- data/app/models/rails_pulse/application_record.rb +7 -0
- data/app/models/rails_pulse/component_cache_key.rb +33 -0
- data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +27 -0
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +37 -0
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +59 -0
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +45 -0
- data/app/models/rails_pulse/operation.rb +87 -0
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +52 -0
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +57 -0
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +71 -0
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +112 -0
- data/app/models/rails_pulse/query.rb +58 -0
- data/app/models/rails_pulse/request.rb +64 -0
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +99 -0
- data/app/models/rails_pulse/requests/charts/operations_chart.rb +35 -0
- data/app/models/rails_pulse/route.rb +77 -0
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +54 -0
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +73 -0
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +73 -0
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +59 -0
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +115 -0
- data/app/models/rails_pulse/routes/tables/index.rb +63 -0
- data/app/services/rails_pulse/sql_query_normalizer.rb +124 -0
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +19 -0
- data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +44 -0
- data/app/views/layouts/rails_pulse/application.html.erb +72 -0
- data/app/views/rails_pulse/caches/show.html.erb +9 -0
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +12 -0
- data/app/views/rails_pulse/components/_code_panel.html.erb +12 -0
- data/app/views/rails_pulse/components/_metric_card.html.erb +55 -0
- data/app/views/rails_pulse/components/_metric_row.html.erb +9 -0
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +241 -0
- data/app/views/rails_pulse/components/_panel.html.erb +56 -0
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +15 -0
- data/app/views/rails_pulse/components/_table.html.erb +50 -0
- data/app/views/rails_pulse/components/_table_head.html.erb +20 -0
- data/app/views/rails_pulse/components/_table_pagination.html.erb +45 -0
- data/app/views/rails_pulse/components/_time_period.html.erb +16 -0
- data/app/views/rails_pulse/csp_test/show.html.erb +207 -0
- data/app/views/rails_pulse/dashboard/charts/_bar_chart.html.erb +1 -0
- data/app/views/rails_pulse/dashboard/index.html.erb +64 -0
- data/app/views/rails_pulse/dashboard/tables/_routes_table.html.erb +32 -0
- data/app/views/rails_pulse/dashboard/tables/_standard_table.html.erb +1 -0
- data/app/views/rails_pulse/operations/_operation_analysis_application.html.erb +43 -0
- data/app/views/rails_pulse/operations/_operation_analysis_database.html.erb +12 -0
- data/app/views/rails_pulse/operations/_operation_analysis_generic.html.erb +15 -0
- data/app/views/rails_pulse/operations/_operation_analysis_other.html.erb +69 -0
- data/app/views/rails_pulse/operations/_operation_analysis_view.html.erb +39 -0
- data/app/views/rails_pulse/operations/show.html.erb +79 -0
- data/app/views/rails_pulse/queries/_show_table.html.erb +19 -0
- data/app/views/rails_pulse/queries/_table.html.erb +31 -0
- data/app/views/rails_pulse/queries/index.html.erb +64 -0
- data/app/views/rails_pulse/queries/show.html.erb +86 -0
- data/app/views/rails_pulse/requests/_operations.html.erb +85 -0
- data/app/views/rails_pulse/requests/_table.html.erb +31 -0
- data/app/views/rails_pulse/requests/index.html.erb +64 -0
- data/app/views/rails_pulse/requests/show.html.erb +44 -0
- data/app/views/rails_pulse/routes/_table.html.erb +29 -0
- data/app/views/rails_pulse/routes/index.html.erb +65 -0
- data/app/views/rails_pulse/routes/show.html.erb +67 -0
- data/app/views/rails_pulse/skeletons/_chart.html.erb +3 -0
- data/app/views/rails_pulse/skeletons/_metric_card.html.erb +20 -0
- data/app/views/rails_pulse/skeletons/_panel.html.erb +19 -0
- data/app/views/rails_pulse/skeletons/_table.html.erb +8 -0
- data/config/importmap.rb +12 -0
- data/config/initializers/rails_charts_csp_patch.rb +83 -0
- data/config/initializers/rails_pulse.rb +198 -0
- data/config/routes.rb +16 -0
- data/db/migrate/20250227235904_create_routes.rb +12 -0
- data/db/migrate/20250227235915_create_requests.rb +19 -0
- data/db/migrate/20250228000000_create_queries.rb +14 -0
- data/db/migrate/20250228000056_create_operations.rb +24 -0
- data/lib/generators/rails_pulse/install_generator.rb +17 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +198 -0
- data/lib/rails_pulse/cleanup_service.rb +212 -0
- data/lib/rails_pulse/configuration.rb +176 -0
- data/lib/rails_pulse/engine.rb +88 -0
- data/lib/rails_pulse/middleware/asset_server.rb +84 -0
- data/lib/rails_pulse/middleware/request_collector.rb +120 -0
- data/lib/rails_pulse/migration.rb +29 -0
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +280 -0
- data/lib/rails_pulse/version.rb +3 -0
- data/lib/rails_pulse.rb +38 -0
- data/lib/tasks/rails_pulse_tasks.rake +138 -0
- data/public/rails-pulse-assets/csp-test.js +110 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js +89 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +13 -0
- data/public/rails-pulse-assets/rails-pulse.css +1 -0
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -0
- data/public/rails-pulse-assets/rails-pulse.js +183 -0
- data/public/rails-pulse-assets/rails-pulse.js.map +7 -0
- metadata +339 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
module CachedComponentHelper
|
3
|
+
def cached_component(options)
|
4
|
+
# cache_key = ComponentCacheKey.build(options[:id], options[:context])
|
5
|
+
|
6
|
+
# Add refresh action for panels if requested
|
7
|
+
if options[:refresh_action] && options[:component] == "panel"
|
8
|
+
options[:actions] ||= []
|
9
|
+
options[:actions] << refresh_action_params(options[:id], options[:context], options[:content_partial])
|
10
|
+
end
|
11
|
+
|
12
|
+
# if Rails.cache.exist?(cache_key)
|
13
|
+
if false
|
14
|
+
render_cached_content(options)
|
15
|
+
else
|
16
|
+
render_skeleton_with_frame(options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def render_cached_content(options)
|
23
|
+
cache_key = ComponentCacheKey.build(options[:id], options[:context])
|
24
|
+
cached_data = Rails.cache.read(cache_key)
|
25
|
+
@component_data = cached_data[:component_data]
|
26
|
+
@cached_at = cached_data[:cached_at]
|
27
|
+
component_options = cached_data[:component_options] || {}
|
28
|
+
|
29
|
+
# Wrap the cached content in a Turbo Frame so it can be refreshed using a refresh link in the component
|
30
|
+
turbo_frame_tag "#{options[:id]}_#{options[:component]}", class: options[:class] do
|
31
|
+
render "rails_pulse/components/#{options[:component]}", component_options
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def render_skeleton_with_frame(options)
|
36
|
+
# Store component options temporarily so CachesController can access them
|
37
|
+
cache_key = ComponentCacheKey.build(options[:id], options[:context])
|
38
|
+
Rails.cache.write(cache_key, component_options: options, expires_in: 5.minutes)
|
39
|
+
path_options = options.slice :id, :context
|
40
|
+
|
41
|
+
turbo_frame_tag "#{options[:id]}_#{options[:component]}",
|
42
|
+
src: rails_pulse.cache_path(**path_options),
|
43
|
+
loading: "eager",
|
44
|
+
class: options[:class] do
|
45
|
+
render "rails_pulse/skeletons/#{options[:component]}", options
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def refresh_action_params(id, context, content_partial)
|
50
|
+
refresh_params = {
|
51
|
+
id: id,
|
52
|
+
component_type: "panel",
|
53
|
+
refresh: true
|
54
|
+
}
|
55
|
+
|
56
|
+
# Include content_partial in refresh URL if available
|
57
|
+
refresh_params[:content_partial] = content_partial if content_partial
|
58
|
+
|
59
|
+
{
|
60
|
+
url: rails_pulse.cache_path(refresh_params),
|
61
|
+
icon: "refresh-cw",
|
62
|
+
title: "Refresh data",
|
63
|
+
data: {
|
64
|
+
controller: "rails-pulse--timezone",
|
65
|
+
rails_pulse__timezone_target_frame_value: "#{id}_panel",
|
66
|
+
rails_pulse__timezone_cached_at_value: Time.current.iso8601,
|
67
|
+
turbo_frame: "#{id}_panel",
|
68
|
+
turbo_prefetch: "false"
|
69
|
+
}
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
module ChartFormatters
|
3
|
+
def self.occurred_at_as_time_or_date(time_diff_hours)
|
4
|
+
if time_diff_hours <= 25
|
5
|
+
<<~JS
|
6
|
+
function(value) {
|
7
|
+
const date = new Date(value * 1000);
|
8
|
+
return date.getHours().toString().padStart(2, '0') + ':00';
|
9
|
+
}
|
10
|
+
JS
|
11
|
+
else
|
12
|
+
<<~JS
|
13
|
+
function(value) {
|
14
|
+
const date = new Date(value * 1000);
|
15
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
16
|
+
}
|
17
|
+
JS
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.tooltip_as_time_or_date_with_marker(time_diff_hours)
|
22
|
+
if time_diff_hours <= 25
|
23
|
+
<<~JS
|
24
|
+
function(params) {
|
25
|
+
const data = params[0];
|
26
|
+
const date = new Date(data.axisValue * 1000);
|
27
|
+
const dateString = date.getHours().toString().padStart(2, '0') + ':00';
|
28
|
+
return `${dateString} <br /> ${data.marker} ${parseInt(data.data.value)} ms`;
|
29
|
+
}
|
30
|
+
JS
|
31
|
+
else
|
32
|
+
<<~JS
|
33
|
+
function(params) {
|
34
|
+
const data = params[0];
|
35
|
+
const date = new Date(data.axisValue * 1000);
|
36
|
+
const dateString = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
37
|
+
return `${dateString} <br /> ${data.marker} ${parseInt(data.data.value)} ms`;
|
38
|
+
}
|
39
|
+
JS
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
module ChartHelper
|
3
|
+
# Base chart options shared across all chart types
|
4
|
+
def base_chart_options(units: nil, zoom: false)
|
5
|
+
{
|
6
|
+
tooltip: {
|
7
|
+
trigger: "axis",
|
8
|
+
axisPointer: { type: "shadow" }
|
9
|
+
},
|
10
|
+
toolbox: {
|
11
|
+
feature: { saveAsImage: { show: false } }
|
12
|
+
},
|
13
|
+
xAxis: {
|
14
|
+
axisLine: { show: false },
|
15
|
+
axisTick: { show: false }
|
16
|
+
},
|
17
|
+
yAxis: {
|
18
|
+
splitArea: { show: false },
|
19
|
+
axisLabel: {
|
20
|
+
formatter: "{value} #{units}"
|
21
|
+
}
|
22
|
+
},
|
23
|
+
grid: {
|
24
|
+
left: "0",
|
25
|
+
right: "2%",
|
26
|
+
bottom: (zoom ? "60" : "0"),
|
27
|
+
top: "10%",
|
28
|
+
containLabel: true
|
29
|
+
},
|
30
|
+
animation: false
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def bar_chart_options(units: nil, zoom: false, chart_start: 0, chart_end: 100, xaxis_formatter: nil, tooltip_formatter: nil, zoom_start: nil, zoom_end: nil, chart_data: nil)
|
35
|
+
options = base_chart_options(units: units, zoom: zoom).deep_merge({
|
36
|
+
series: {
|
37
|
+
itemStyle: { borderRadius: [ 5, 5, 5, 5 ] }
|
38
|
+
}
|
39
|
+
})
|
40
|
+
|
41
|
+
apply_tooltip_formatter(options, tooltip_formatter)
|
42
|
+
apply_xaxis_formatter(options, xaxis_formatter)
|
43
|
+
apply_zoom_configuration(options, zoom, zoom_start, zoom_end, chart_data)
|
44
|
+
|
45
|
+
options
|
46
|
+
end
|
47
|
+
|
48
|
+
def line_chart_options(units: nil, zoom: false, chart_start: 0, chart_end: 100, xaxis_formatter: nil, tooltip_formatter: nil, zoom_start: nil, zoom_end: nil, chart_data: nil)
|
49
|
+
options = base_chart_options(units: units, zoom: zoom).deep_merge({
|
50
|
+
series: {
|
51
|
+
smooth: true,
|
52
|
+
lineStyle: { width: 3 },
|
53
|
+
symbol: "circle",
|
54
|
+
symbolSize: 8
|
55
|
+
}
|
56
|
+
})
|
57
|
+
|
58
|
+
apply_tooltip_formatter(options, tooltip_formatter)
|
59
|
+
apply_xaxis_formatter(options, xaxis_formatter)
|
60
|
+
apply_zoom_configuration(options, zoom, zoom_start, zoom_end, chart_data)
|
61
|
+
|
62
|
+
options
|
63
|
+
end
|
64
|
+
|
65
|
+
def sparkline_chart_options
|
66
|
+
base_chart_options.deep_merge({
|
67
|
+
series: {
|
68
|
+
type: "line",
|
69
|
+
smooth: true,
|
70
|
+
lineStyle: { width: 2 },
|
71
|
+
symbol: "none"
|
72
|
+
},
|
73
|
+
yAxis: { show: false },
|
74
|
+
xAxis: { splitLine: { show: false } },
|
75
|
+
grid: { show: false }
|
76
|
+
})
|
77
|
+
end
|
78
|
+
|
79
|
+
def area_chart_options
|
80
|
+
base_chart_options.deep_merge({
|
81
|
+
series: {
|
82
|
+
smooth: true,
|
83
|
+
lineStyle: { width: 3 },
|
84
|
+
symbol: "roundRect",
|
85
|
+
symbolSize: 8
|
86
|
+
}
|
87
|
+
})
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def apply_tooltip_formatter(options, tooltip_formatter)
|
93
|
+
return unless tooltip_formatter.present?
|
94
|
+
|
95
|
+
options[:tooltip][:formatter] = RailsCharts.js(tooltip_formatter)
|
96
|
+
end
|
97
|
+
|
98
|
+
def apply_xaxis_formatter(options, xaxis_formatter)
|
99
|
+
return unless xaxis_formatter.present?
|
100
|
+
|
101
|
+
options[:xAxis][:axisLabel] ||= { formatter: RailsCharts.js(xaxis_formatter) }
|
102
|
+
end
|
103
|
+
|
104
|
+
def apply_zoom_configuration(options, zoom, zoom_start, zoom_end, chart_data)
|
105
|
+
return unless zoom
|
106
|
+
|
107
|
+
zoom_config = {
|
108
|
+
type: "slider",
|
109
|
+
height: 20,
|
110
|
+
bottom: 10,
|
111
|
+
showDetail: false
|
112
|
+
}
|
113
|
+
|
114
|
+
# Initialize zoom range if zoom parameters are provided
|
115
|
+
if zoom_start.present? && zoom_end.present? && chart_data.present?
|
116
|
+
# Find closest matching timestamps in the actual chart data
|
117
|
+
# Chart data is a hash like: { 1234567890 => { value: 123.45 } }
|
118
|
+
chart_timestamps = chart_data.keys
|
119
|
+
|
120
|
+
if chart_timestamps.any?
|
121
|
+
closest_start = chart_timestamps.min_by { |ts| (ts - zoom_start).abs }
|
122
|
+
closest_end = chart_timestamps.min_by { |ts| (ts - zoom_end).abs }
|
123
|
+
|
124
|
+
# Find the array indices of these timestamps
|
125
|
+
start_index = chart_timestamps.index(closest_start)
|
126
|
+
end_index = chart_timestamps.index(closest_end)
|
127
|
+
|
128
|
+
# Use array indices for dataZoom instead of timestamp values
|
129
|
+
zoom_config[:startValue] = start_index
|
130
|
+
zoom_config[:endValue] = end_index
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
options[:dataZoom] = [
|
135
|
+
zoom_config,
|
136
|
+
{ type: "inside" }
|
137
|
+
]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
module FormattingHelper
|
3
|
+
def human_readable_occurred_at(occurred_at)
|
4
|
+
return "" unless occurred_at.present?
|
5
|
+
time = occurred_at.is_a?(String) ? Time.parse(occurred_at) : occurred_at
|
6
|
+
time.strftime("%b %d, %Y %l:%M %p")
|
7
|
+
end
|
8
|
+
|
9
|
+
def time_ago_in_words(time)
|
10
|
+
return "Unknown" if time.blank?
|
11
|
+
|
12
|
+
# Convert to Time object if it's a string
|
13
|
+
time = Time.parse(time.to_s) if time.is_a?(String)
|
14
|
+
|
15
|
+
seconds_ago = Time.current - time
|
16
|
+
|
17
|
+
case seconds_ago
|
18
|
+
when 0..59
|
19
|
+
"#{seconds_ago.to_i}s ago"
|
20
|
+
when 60..3599
|
21
|
+
"#{(seconds_ago / 60).to_i}m ago"
|
22
|
+
when 3600..86399
|
23
|
+
"#{(seconds_ago / 3600).to_i}h ago"
|
24
|
+
else
|
25
|
+
"#{(seconds_ago / 86400).to_i}d ago"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,279 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
module StatusHelper
|
3
|
+
def route_status_indicator(status_value)
|
4
|
+
case status_value.to_i
|
5
|
+
when 0
|
6
|
+
# Healthy routes show no icon to reduce visual clutter
|
7
|
+
""
|
8
|
+
when 1
|
9
|
+
content_tag(
|
10
|
+
:span,
|
11
|
+
lucide_icon("alert-triangle", width: "16", height: "16", class: "text-yellow-600"),
|
12
|
+
title: "Warning - Response time > #{RailsPulse.configuration.route_thresholds[:slow]} ms"
|
13
|
+
)
|
14
|
+
when 2
|
15
|
+
content_tag(
|
16
|
+
:span,
|
17
|
+
lucide_icon("alert-circle", width: "16", height: "16", class: "text-orange-600"),
|
18
|
+
title: "Slow - Response time > #{RailsPulse.configuration.route_thresholds[:very_slow]} ms"
|
19
|
+
)
|
20
|
+
when 3
|
21
|
+
content_tag(
|
22
|
+
:span,
|
23
|
+
lucide_icon("x-circle", width: "16", height: "16", class: "text-red-600"),
|
24
|
+
title: "Critical - Response time > #{RailsPulse.configuration.route_thresholds[:critical]} ms"
|
25
|
+
)
|
26
|
+
else
|
27
|
+
content_tag(
|
28
|
+
:span,
|
29
|
+
lucide_icon("help-circle", width: "16", height: "16", class: "text-gray-400"),
|
30
|
+
title: "Unknown status"
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def request_status_indicator(duration)
|
36
|
+
thresholds = RailsPulse.configuration.request_thresholds
|
37
|
+
status_value = case duration.to_i
|
38
|
+
when 0...thresholds[:slow]
|
39
|
+
0 # Healthy
|
40
|
+
when thresholds[:slow]...thresholds[:very_slow]
|
41
|
+
1 # Warning
|
42
|
+
when thresholds[:very_slow]...thresholds[:critical]
|
43
|
+
2 # Slow
|
44
|
+
else
|
45
|
+
3 # Critical
|
46
|
+
end
|
47
|
+
|
48
|
+
case status_value
|
49
|
+
when 0
|
50
|
+
# Healthy requests show no icon to reduce visual clutter
|
51
|
+
""
|
52
|
+
when 1
|
53
|
+
content_tag(
|
54
|
+
:span,
|
55
|
+
lucide_icon("alert-triangle", width: "16", height: "16", class: "text-yellow-600"),
|
56
|
+
title: "Warning - Response time > #{thresholds[:slow]} ms"
|
57
|
+
)
|
58
|
+
when 2
|
59
|
+
content_tag(
|
60
|
+
:span,
|
61
|
+
lucide_icon("alert-circle", width: "16", height: "16", class: "text-orange-600"),
|
62
|
+
title: "Slow - Response time > #{thresholds[:very_slow]} ms"
|
63
|
+
)
|
64
|
+
when 3
|
65
|
+
content_tag(
|
66
|
+
:span,
|
67
|
+
lucide_icon("x-circle", width: "16", height: "16", class: "text-red-600"),
|
68
|
+
title: "Critical - Response time > #{thresholds[:critical]} ms"
|
69
|
+
)
|
70
|
+
else
|
71
|
+
content_tag(
|
72
|
+
:span,
|
73
|
+
lucide_icon("help-circle", width: "16", height: "16", class: "text-gray-400"),
|
74
|
+
title: "Unknown status"
|
75
|
+
)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def query_status_indicator(avg_duration)
|
80
|
+
thresholds = RailsPulse.configuration.query_thresholds
|
81
|
+
status_value = case avg_duration.to_f
|
82
|
+
when 0...thresholds[:slow]
|
83
|
+
0 # Healthy
|
84
|
+
when thresholds[:slow]...thresholds[:very_slow]
|
85
|
+
1 # Warning
|
86
|
+
when thresholds[:very_slow]...thresholds[:critical]
|
87
|
+
2 # Slow
|
88
|
+
else
|
89
|
+
3 # Critical
|
90
|
+
end
|
91
|
+
|
92
|
+
case status_value
|
93
|
+
when 0
|
94
|
+
# Healthy queries show no icon to reduce visual clutter
|
95
|
+
""
|
96
|
+
when 1
|
97
|
+
content_tag(
|
98
|
+
:span,
|
99
|
+
lucide_icon("alert-triangle", width: "16", height: "16", class: "text-yellow-600"),
|
100
|
+
title: "Warning - Query time > #{thresholds[:slow]} ms"
|
101
|
+
)
|
102
|
+
when 2
|
103
|
+
content_tag(
|
104
|
+
:span,
|
105
|
+
lucide_icon("alert-circle", width: "16", height: "16", class: "text-orange-600"),
|
106
|
+
title: "Slow - Query time > #{thresholds[:very_slow]} ms"
|
107
|
+
)
|
108
|
+
when 3
|
109
|
+
content_tag(
|
110
|
+
:span,
|
111
|
+
lucide_icon("x-circle", width: "16", height: "16", class: "text-red-600"),
|
112
|
+
title: "Critical - Query time > #{thresholds[:critical]} ms"
|
113
|
+
)
|
114
|
+
else
|
115
|
+
content_tag(
|
116
|
+
:span,
|
117
|
+
lucide_icon("help-circle", width: "16", height: "16", class: "text-gray-400"),
|
118
|
+
title: "Unknown status"
|
119
|
+
)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def operation_status_indicator(operation)
|
124
|
+
# Define operation-specific thresholds
|
125
|
+
thresholds = case operation.operation_type
|
126
|
+
when "sql"
|
127
|
+
{ slow: 50, very_slow: 100, critical: 500 }
|
128
|
+
when "template", "partial", "layout", "collection"
|
129
|
+
{ slow: 50, very_slow: 150, critical: 300 }
|
130
|
+
when "controller"
|
131
|
+
{ slow: 200, very_slow: 500, critical: 1000 }
|
132
|
+
when "cache_read", "cache_write"
|
133
|
+
{ slow: 10, very_slow: 50, critical: 100 }
|
134
|
+
when "http"
|
135
|
+
{ slow: 500, very_slow: 1000, critical: 3000 }
|
136
|
+
when "job"
|
137
|
+
{ slow: 1000, very_slow: 5000, critical: 10000 }
|
138
|
+
when "mailer"
|
139
|
+
{ slow: 500, very_slow: 2000, critical: 5000 }
|
140
|
+
when "storage"
|
141
|
+
{ slow: 500, very_slow: 1000, critical: 3000 }
|
142
|
+
else
|
143
|
+
{ slow: 100, very_slow: 300, critical: 1000 }
|
144
|
+
end
|
145
|
+
|
146
|
+
duration = operation.duration.to_f
|
147
|
+
status_value = case duration
|
148
|
+
when 0...thresholds[:slow]
|
149
|
+
0 # Healthy
|
150
|
+
when thresholds[:slow]...thresholds[:very_slow]
|
151
|
+
1 # Warning
|
152
|
+
when thresholds[:very_slow]...thresholds[:critical]
|
153
|
+
2 # Slow
|
154
|
+
else
|
155
|
+
3 # Critical
|
156
|
+
end
|
157
|
+
|
158
|
+
case status_value
|
159
|
+
when 0
|
160
|
+
# Healthy operations show no icon to reduce visual clutter
|
161
|
+
""
|
162
|
+
when 1
|
163
|
+
content_tag(
|
164
|
+
:span,
|
165
|
+
lucide_icon("alert-triangle", width: "16", height: "16", class: "text-yellow-600"),
|
166
|
+
title: "Warning - Operation time > #{thresholds[:slow]} ms"
|
167
|
+
)
|
168
|
+
when 2
|
169
|
+
content_tag(
|
170
|
+
:span,
|
171
|
+
lucide_icon("alert-circle", width: "16", height: "16", class: "text-orange-600"),
|
172
|
+
title: "Slow - Operation time > #{thresholds[:very_slow]} ms"
|
173
|
+
)
|
174
|
+
when 3
|
175
|
+
content_tag(
|
176
|
+
:span,
|
177
|
+
lucide_icon("x-circle", width: "16", height: "16", class: "text-red-600"),
|
178
|
+
title: "Critical - Operation time > #{thresholds[:critical]} ms"
|
179
|
+
)
|
180
|
+
else
|
181
|
+
content_tag(
|
182
|
+
:span,
|
183
|
+
lucide_icon("help-circle", width: "16", height: "16", class: "text-gray-400"),
|
184
|
+
title: "Unknown status"
|
185
|
+
)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def operations_performance_breakdown(operations)
|
190
|
+
return { database: 0, view: 0, application: 0, other: 0 } if operations.empty?
|
191
|
+
|
192
|
+
total_duration = operations.sum(&:duration).to_f
|
193
|
+
return { database: 0, view: 0, application: 0, other: 0 } if total_duration.zero?
|
194
|
+
|
195
|
+
breakdown = operations.group_by { |op| categorize_operation(op.operation_type) }
|
196
|
+
.transform_values { |ops| ops.sum(&:duration) }
|
197
|
+
|
198
|
+
{
|
199
|
+
database: ((breakdown[:database] || 0) / total_duration * 100).round(1),
|
200
|
+
view: ((breakdown[:view] || 0) / total_duration * 100).round(1),
|
201
|
+
application: ((breakdown[:application] || 0) / total_duration * 100).round(1),
|
202
|
+
other: ((breakdown[:other] || 0) / total_duration * 100).round(1)
|
203
|
+
}
|
204
|
+
end
|
205
|
+
|
206
|
+
def categorize_operation(operation_type)
|
207
|
+
case operation_type
|
208
|
+
when "sql"
|
209
|
+
:database
|
210
|
+
when "template", "partial", "layout", "collection"
|
211
|
+
:view
|
212
|
+
when "controller"
|
213
|
+
:application
|
214
|
+
else
|
215
|
+
:other
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def operation_category_label(operation_type)
|
220
|
+
case categorize_operation(operation_type)
|
221
|
+
when :database
|
222
|
+
"Database"
|
223
|
+
when :view
|
224
|
+
"View Rendering"
|
225
|
+
when :application
|
226
|
+
"Application Logic"
|
227
|
+
else
|
228
|
+
"Other Operations"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def performance_badge_class(percentile)
|
233
|
+
case percentile
|
234
|
+
when 0..50
|
235
|
+
"badge--positive"
|
236
|
+
when 51..75
|
237
|
+
"badge--warning"
|
238
|
+
when 76..90
|
239
|
+
"badge--negative"
|
240
|
+
else
|
241
|
+
"badge--critical"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def rescue_template_missing
|
246
|
+
yield
|
247
|
+
true
|
248
|
+
rescue ActionView::MissingTemplate
|
249
|
+
false
|
250
|
+
end
|
251
|
+
|
252
|
+
def truncate_sql(sql, length: 100)
|
253
|
+
return sql if sql.length <= length
|
254
|
+
sql.truncate(length)
|
255
|
+
end
|
256
|
+
|
257
|
+
def event_color(operation_type)
|
258
|
+
case operation_type
|
259
|
+
when "sql" then "#92c282;"
|
260
|
+
when "template", "partial", "layout", "collection" then "#b77cbf"
|
261
|
+
when "controller" then "#00adc4"
|
262
|
+
else "gray"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def duration_options(type = :route)
|
267
|
+
thresholds = RailsPulse.configuration.public_send("#{type}_thresholds")
|
268
|
+
|
269
|
+
first_label = "All #{type.to_s.humanize.pluralize}"
|
270
|
+
|
271
|
+
[
|
272
|
+
[ first_label, :all ],
|
273
|
+
[ "Slow (≥ #{thresholds[:slow]}ms)", :slow ],
|
274
|
+
[ "Very Slow (≥ #{thresholds[:very_slow]}ms)", :very_slow ],
|
275
|
+
[ "Critical (≥ #{thresholds[:critical]}ms)", :critical ]
|
276
|
+
]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
module TableHelper
|
3
|
+
def render_cell_content(row_data, column)
|
4
|
+
value = row_data[column[:field]]
|
5
|
+
|
6
|
+
# Handle links
|
7
|
+
if column[:link_to] && row_data[column[:link_to]]
|
8
|
+
# Direct link provided
|
9
|
+
link_to value, row_data[column[:link_to]], data: { turbo_frame: "_top" }
|
10
|
+
elsif column[:link_field] && row_data[column[:link_field]]
|
11
|
+
# Generate link based on field type and ID
|
12
|
+
case column[:link_field]
|
13
|
+
when :query_id
|
14
|
+
link_to value, query_path(row_data[column[:link_field]]), data: { turbo_frame: "_top" }
|
15
|
+
when :route_id
|
16
|
+
link_to value, route_path(row_data[column[:link_field]]), data: { turbo_frame: "_top" }
|
17
|
+
else
|
18
|
+
value
|
19
|
+
end
|
20
|
+
elsif column[:format] == :percentage && value.is_a?(Numeric)
|
21
|
+
"#{value > 0 ? '+' : ''}#{value}%"
|
22
|
+
elsif value.is_a?(Numeric) && column[:field].to_s.include?("time")
|
23
|
+
"#{value.round(0)} ms"
|
24
|
+
else
|
25
|
+
value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def cell_highlight_class(row_data, column)
|
30
|
+
return "" unless column[:highlight]
|
31
|
+
|
32
|
+
case column[:highlight]
|
33
|
+
when :trend
|
34
|
+
trend = row_data[:trend]
|
35
|
+
case trend
|
36
|
+
when "worse" then "highlight-red"
|
37
|
+
when "better" then "highlight-green"
|
38
|
+
else ""
|
39
|
+
end
|
40
|
+
when :percentage_change
|
41
|
+
change = row_data[:percentage_change]
|
42
|
+
if change && change > 5
|
43
|
+
"highlight-red"
|
44
|
+
elsif change && change < -5
|
45
|
+
"highlight-green"
|
46
|
+
else
|
47
|
+
""
|
48
|
+
end
|
49
|
+
else
|
50
|
+
""
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|