rails_event_viewer 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/CHANGELOG.md +14 -0
- data/MIT-LICENSE +20 -0
- data/README.md +451 -0
- data/Rakefile +16 -0
- data/app/assets/stylesheets/rails_event_viewer/application.css +32 -0
- data/app/assets/stylesheets/rails_event_viewer/tailwind.css +54 -0
- data/app/controllers/rails_event_viewer/analytics_controller.rb +78 -0
- data/app/controllers/rails_event_viewer/application_controller.rb +84 -0
- data/app/controllers/rails_event_viewer/dashboard_controller.rb +18 -0
- data/app/controllers/rails_event_viewer/event_types_controller.rb +16 -0
- data/app/controllers/rails_event_viewer/events_controller.rb +72 -0
- data/app/controllers/rails_event_viewer/groups_controller.rb +52 -0
- data/app/helpers/rails_event_viewer/application_helper.rb +163 -0
- data/app/models/rails_event_viewer/application_record.rb +5 -0
- data/app/models/rails_event_viewer/entry.rb +34 -0
- data/app/views/layouts/rails_event_viewer/application.html.erb +45 -0
- data/app/views/rails_event_viewer/analytics/overview.html.erb +134 -0
- data/app/views/rails_event_viewer/dashboard/index.html.erb +113 -0
- data/app/views/rails_event_viewer/event_types/index.html.erb +59 -0
- data/app/views/rails_event_viewer/event_types/show.html.erb +102 -0
- data/app/views/rails_event_viewer/events/_event_row.html.erb +42 -0
- data/app/views/rails_event_viewer/events/_filters_sidebar.html.erb +66 -0
- data/app/views/rails_event_viewer/events/index.html.erb +65 -0
- data/app/views/rails_event_viewer/events/show.html.erb +155 -0
- data/app/views/rails_event_viewer/groups/index.html.erb +115 -0
- data/app/views/rails_event_viewer/groups/show.html.erb +134 -0
- data/app/views/rails_event_viewer/shared/_navigation.html.erb +47 -0
- data/app/views/rails_event_viewer/shared/_pagination.html.erb +60 -0
- data/app/views/rails_event_viewer/shared/_stat_card.html.erb +36 -0
- data/config/routes.rb +18 -0
- data/db/migrate/20241208000001_create_rails_event_viewer_entries.rb +36 -0
- data/lib/generators/rails_event_viewer/install/USAGE +33 -0
- data/lib/generators/rails_event_viewer/install/install_generator.rb +86 -0
- data/lib/generators/rails_event_viewer/install/templates/create_rails_event_viewer_entries.rb.erb +35 -0
- data/lib/generators/rails_event_viewer/install/templates/initializer.rb.erb +90 -0
- data/lib/rails_event_viewer/adapter.rb +93 -0
- data/lib/rails_event_viewer/adapters/active_record.rb +227 -0
- data/lib/rails_event_viewer/adapters/concerns/in_memory_filtering.rb +136 -0
- data/lib/rails_event_viewer/adapters/memory.rb +128 -0
- data/lib/rails_event_viewer/adapters/null.rb +63 -0
- data/lib/rails_event_viewer/adapters/redis.rb +213 -0
- data/lib/rails_event_viewer/engine.rb +24 -0
- data/lib/rails_event_viewer/events_relation.rb +130 -0
- data/lib/rails_event_viewer/json_query.rb +111 -0
- data/lib/rails_event_viewer/pagy_countable.rb +31 -0
- data/lib/rails_event_viewer/subscriber.rb +255 -0
- data/lib/rails_event_viewer/time_utils.rb +24 -0
- data/lib/rails_event_viewer/version.rb +3 -0
- data/lib/rails_event_viewer.rb +122 -0
- data/lib/tasks/rails_event_viewer_tasks.rake +81 -0
- metadata +151 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<% content_for(:title, "Groups - Rails Event Viewer") %>
|
|
2
|
+
|
|
3
|
+
<div class="space-y-6">
|
|
4
|
+
<div class="flex justify-between items-center">
|
|
5
|
+
<div>
|
|
6
|
+
<h1 class="text-2xl font-bold text-gray-900">Event Groups</h1>
|
|
7
|
+
<p class="mt-1 text-sm text-gray-500">
|
|
8
|
+
Track related events grouped by context or tags
|
|
9
|
+
</p>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
14
|
+
<%= form_with url: rails_event_viewer.groups_path, method: :get, class: "flex items-center space-x-4" do |f| %>
|
|
15
|
+
<div class="flex items-center space-x-2">
|
|
16
|
+
<%= f.label :key, "Group by:", class: "text-sm font-medium text-gray-700" %>
|
|
17
|
+
<%= f.select :key,
|
|
18
|
+
options_for_select(@group_keys.map { |k| [k.to_s.titleize, k] }, @selected_key),
|
|
19
|
+
{},
|
|
20
|
+
class: "rounded-md border border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 py-2 px-3" %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="flex items-center space-x-2">
|
|
24
|
+
<%= f.label :source, "Source:", class: "text-sm font-medium text-gray-700" %>
|
|
25
|
+
<%= f.select :source,
|
|
26
|
+
options_for_select([["Context", :context], ["Tags", :tags]], @source),
|
|
27
|
+
{},
|
|
28
|
+
class: "rounded-md border border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 py-2 px-3" %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<%= f.submit "Apply",
|
|
32
|
+
class: "bg-indigo-600 text-white px-4 py-2 rounded-md font-medium hover:bg-indigo-700 cursor-pointer" %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<% if @group_keys.empty? %>
|
|
37
|
+
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
|
38
|
+
<div class="flex">
|
|
39
|
+
<svg class="h-5 w-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
|
40
|
+
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
|
41
|
+
</svg>
|
|
42
|
+
<div class="ml-3">
|
|
43
|
+
<h3 class="text-sm font-medium text-yellow-800">No group keys configured</h3>
|
|
44
|
+
<p class="mt-1 text-sm text-yellow-700">
|
|
45
|
+
Configure group keys in your initializer:
|
|
46
|
+
<code class="bg-yellow-100 px-1 rounded">config.group_keys = [:request_id, :order_id]</code>
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<% elsif @groups.any? %>
|
|
52
|
+
<div class="bg-white shadow rounded-lg overflow-hidden">
|
|
53
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
54
|
+
<thead class="bg-gray-50">
|
|
55
|
+
<tr>
|
|
56
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
57
|
+
<%= @selected_key.to_s.titleize %>
|
|
58
|
+
</th>
|
|
59
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Events</th>
|
|
60
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">First Event</th>
|
|
61
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Event</th>
|
|
62
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Duration</th>
|
|
63
|
+
</tr>
|
|
64
|
+
</thead>
|
|
65
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
66
|
+
<% @groups.each do |group| %>
|
|
67
|
+
<% duration = group[:last_event_at] && group[:first_event_at] ? group[:last_event_at] - group[:first_event_at] : nil %>
|
|
68
|
+
<tr class="hover:bg-gray-50">
|
|
69
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
70
|
+
<%= link_to rails_event_viewer.group_path(group[:value], key: @selected_key, source: @source),
|
|
71
|
+
class: "text-indigo-600 hover:text-indigo-900 font-medium" do %>
|
|
72
|
+
<code class="text-sm bg-gray-100 px-2 py-1 rounded"><%= group[:value] %></code>
|
|
73
|
+
<% end %>
|
|
74
|
+
</td>
|
|
75
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
76
|
+
<%= number_with_delimiter(group[:count]) %>
|
|
77
|
+
</td>
|
|
78
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
79
|
+
<% if group[:first_event_at] %>
|
|
80
|
+
<%= time_ago_with_title(group[:first_event_at]) %>
|
|
81
|
+
<% else %>
|
|
82
|
+
-
|
|
83
|
+
<% end %>
|
|
84
|
+
</td>
|
|
85
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
86
|
+
<% if group[:last_event_at] %>
|
|
87
|
+
<%= time_ago_with_title(group[:last_event_at]) %>
|
|
88
|
+
<% else %>
|
|
89
|
+
-
|
|
90
|
+
<% end %>
|
|
91
|
+
</td>
|
|
92
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
93
|
+
<% if duration %>
|
|
94
|
+
<%= format_duration(duration) %>
|
|
95
|
+
<% else %>
|
|
96
|
+
-
|
|
97
|
+
<% end %>
|
|
98
|
+
</td>
|
|
99
|
+
</tr>
|
|
100
|
+
<% end %>
|
|
101
|
+
</tbody>
|
|
102
|
+
</table>
|
|
103
|
+
</div>
|
|
104
|
+
<% else %>
|
|
105
|
+
<div class="bg-white rounded-lg shadow px-6 py-12 text-center text-gray-500">
|
|
106
|
+
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
107
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
108
|
+
</svg>
|
|
109
|
+
<h3 class="mt-2 text-sm font-medium text-gray-900">No groups found</h3>
|
|
110
|
+
<p class="mt-1 text-sm text-gray-500">
|
|
111
|
+
No events with <code class="bg-gray-100 px-1 rounded"><%= @selected_key %></code> in their <%= @source %> data.
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
<% end %>
|
|
115
|
+
</div>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<% content_for(:title, "Group: #{@value} - Rails Event Viewer") %>
|
|
2
|
+
|
|
3
|
+
<div class="space-y-6">
|
|
4
|
+
<!-- Header -->
|
|
5
|
+
<div class="flex items-center justify-between">
|
|
6
|
+
<div class="flex items-center space-x-4">
|
|
7
|
+
<%= link_to rails_event_viewer.groups_path(key: @key, source: @source), class: "text-gray-400 hover:text-gray-600", aria: { label: "Back to groups" } do %>
|
|
8
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
10
|
+
</svg>
|
|
11
|
+
<% end %>
|
|
12
|
+
<div>
|
|
13
|
+
<p class="text-sm text-gray-500"><%= @key.to_s.titleize %></p>
|
|
14
|
+
<h1 class="text-2xl font-bold text-gray-900">
|
|
15
|
+
<code class="bg-gray-100 px-2 py-1 rounded"><%= @value %></code>
|
|
16
|
+
</h1>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Stats -->
|
|
22
|
+
<div class="grid grid-cols-1 gap-5 sm:grid-cols-4">
|
|
23
|
+
<%= render "rails_event_viewer/shared/stat_card",
|
|
24
|
+
title: "Total Events",
|
|
25
|
+
value: number_with_delimiter(@total_count),
|
|
26
|
+
icon: "database" %>
|
|
27
|
+
<%= render "rails_event_viewer/shared/stat_card",
|
|
28
|
+
title: "Event Types",
|
|
29
|
+
value: @event_names.size,
|
|
30
|
+
icon: "tag" %>
|
|
31
|
+
<%= render "rails_event_viewer/shared/stat_card",
|
|
32
|
+
title: "Duration",
|
|
33
|
+
value: @duration ? format_duration(@duration) : "-",
|
|
34
|
+
icon: "clock" %>
|
|
35
|
+
<%= render "rails_event_viewer/shared/stat_card",
|
|
36
|
+
title: "Source",
|
|
37
|
+
value: @source.to_s.titleize,
|
|
38
|
+
icon: "calendar" %>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- Timeline -->
|
|
42
|
+
<div class="bg-white shadow rounded-lg overflow-hidden">
|
|
43
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
44
|
+
<h3 class="text-lg font-medium text-gray-900">Event Timeline</h3>
|
|
45
|
+
<% if @first_event_at && @last_event_at %>
|
|
46
|
+
<p class="text-sm text-gray-500">
|
|
47
|
+
<%= @first_event_at.strftime("%Y-%m-%d %H:%M:%S") %> to <%= @last_event_at.strftime("%Y-%m-%d %H:%M:%S") %>
|
|
48
|
+
</p>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<% if @events.any? %>
|
|
53
|
+
<div class="relative">
|
|
54
|
+
<div class="absolute left-12 top-0 bottom-0 w-0.5 bg-gray-200"></div>
|
|
55
|
+
|
|
56
|
+
<div class="divide-y divide-gray-100">
|
|
57
|
+
<% @events.each_with_index do |event, index| %>
|
|
58
|
+
<a href="<%= rails_event_viewer.event_path(event_id(event)) %>" class="relative pl-16 pr-6 py-4 hover:bg-gray-50 block">
|
|
59
|
+
<div class="absolute left-10 top-6 w-4 h-4 rounded-full border-2 border-indigo-500 bg-white"></div>
|
|
60
|
+
|
|
61
|
+
<% if @first_event_at && event_occurred_at(event) %>
|
|
62
|
+
<% offset = event_occurred_at(event) - @first_event_at %>
|
|
63
|
+
<div class="absolute left-0 top-4 w-9 text-right">
|
|
64
|
+
<span class="text-xs text-gray-400" title="<%= format_duration(offset) %> from start">
|
|
65
|
+
<% if offset < 0.001 %>
|
|
66
|
+
0
|
|
67
|
+
<% elsif offset < 1 %>
|
|
68
|
+
+<%= (offset * 1000).round %>ms
|
|
69
|
+
<% elsif offset < 60 %>
|
|
70
|
+
+<%= offset.round(1) %>s
|
|
71
|
+
<% else %>
|
|
72
|
+
+<%= (offset / 60).round %>m
|
|
73
|
+
<% end %>
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
<% end %>
|
|
77
|
+
|
|
78
|
+
<div class="flex items-start justify-between">
|
|
79
|
+
<div class="flex-1 min-w-0">
|
|
80
|
+
<!-- Event name badge -->
|
|
81
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
|
|
82
|
+
<%= event_name(event) %>
|
|
83
|
+
</span>
|
|
84
|
+
|
|
85
|
+
<!-- Payload preview -->
|
|
86
|
+
<% payload = event_payload(event) %>
|
|
87
|
+
<% if payload.present? && payload.any? %>
|
|
88
|
+
<div class="mt-2">
|
|
89
|
+
<code class="text-xs bg-gray-100 px-2 py-1 rounded text-gray-600">
|
|
90
|
+
<%= truncate_json(payload, length: 100) %>
|
|
91
|
+
</code>
|
|
92
|
+
</div>
|
|
93
|
+
<% end %>
|
|
94
|
+
|
|
95
|
+
<!-- Tags -->
|
|
96
|
+
<% tags = event_tags(event) %>
|
|
97
|
+
<% if tags.present? && tags.any? %>
|
|
98
|
+
<div class="mt-2 flex flex-wrap gap-1">
|
|
99
|
+
<% tags.first(3).each do |key, value| %>
|
|
100
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700">
|
|
101
|
+
<%= key %>: <%= value.to_s.truncate(20) %>
|
|
102
|
+
</span>
|
|
103
|
+
<% end %>
|
|
104
|
+
<% if tags.size > 3 %>
|
|
105
|
+
<span class="text-xs text-gray-500">+<%= tags.size - 3 %> more</span>
|
|
106
|
+
<% end %>
|
|
107
|
+
</div>
|
|
108
|
+
<% end %>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="ml-4 flex-shrink-0 text-right">
|
|
112
|
+
<p class="text-sm text-gray-500">
|
|
113
|
+
<%= time_ago_with_title(event_occurred_at(event)) %>
|
|
114
|
+
</p>
|
|
115
|
+
<% if event_source_file(event).present? %>
|
|
116
|
+
<p class="text-xs text-gray-400 mt-1">
|
|
117
|
+
<%= event_short_filepath(event) %>:<%= event_source_line(event) %>
|
|
118
|
+
</p>
|
|
119
|
+
<% end %>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</a>
|
|
123
|
+
<% end %>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<%= render "rails_event_viewer/shared/pagination", pagy: @pagy %>
|
|
128
|
+
<% else %>
|
|
129
|
+
<div class="px-6 py-12 text-center text-gray-500">
|
|
130
|
+
No events found in this group.
|
|
131
|
+
</div>
|
|
132
|
+
<% end %>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<nav class="bg-indigo-600">
|
|
2
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
3
|
+
<div class="flex h-16 items-center justify-between">
|
|
4
|
+
<div class="flex items-center">
|
|
5
|
+
<div class="flex-shrink-0">
|
|
6
|
+
<%= link_to rails_event_viewer.root_path, class: "text-white text-xl font-bold" do %>
|
|
7
|
+
Event Viewer
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="ml-10 flex items-baseline space-x-4">
|
|
11
|
+
<%= link_to "Dashboard", rails_event_viewer.root_path,
|
|
12
|
+
class: nav_link_class("dashboard") %>
|
|
13
|
+
<%= link_to "Events", rails_event_viewer.events_path,
|
|
14
|
+
class: nav_link_class("events") %>
|
|
15
|
+
<%= link_to "Event Types", rails_event_viewer.event_types_path,
|
|
16
|
+
class: nav_link_class("event_types") %>
|
|
17
|
+
<%= link_to "Groups", rails_event_viewer.groups_path,
|
|
18
|
+
class: nav_link_class("groups") %>
|
|
19
|
+
<%= link_to "Analytics", rails_event_viewer.analytics_overview_path,
|
|
20
|
+
class: nav_link_class("analytics") %>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="flex items-center space-x-4">
|
|
25
|
+
<!-- Search -->
|
|
26
|
+
<%= form_with url: rails_event_viewer.search_events_path, method: :get, class: "relative", data: { turbo_frame: "_top" } do |f| %>
|
|
27
|
+
<div class="relative">
|
|
28
|
+
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
|
29
|
+
<svg class="h-4 w-4 text-indigo-200" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
30
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
31
|
+
</svg>
|
|
32
|
+
</div>
|
|
33
|
+
<%= f.search_field :q,
|
|
34
|
+
value: params[:q],
|
|
35
|
+
placeholder: "Search events...",
|
|
36
|
+
class: "w-64 rounded-md bg-indigo-500 py-2 pl-10 pr-3 text-sm text-white placeholder-indigo-200 focus:bg-white focus:text-gray-900 focus:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-white" %>
|
|
37
|
+
</div>
|
|
38
|
+
<% end %>
|
|
39
|
+
|
|
40
|
+
<!-- Timezone indicator -->
|
|
41
|
+
<div class="text-indigo-200 text-sm" title="Current timezone">
|
|
42
|
+
<%= Time.zone.name %>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</nav>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<% if pagy.pages > 1 %>
|
|
2
|
+
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
|
3
|
+
<div class="flex-1 flex items-center justify-between">
|
|
4
|
+
<div>
|
|
5
|
+
<p class="text-sm text-gray-700">
|
|
6
|
+
Showing
|
|
7
|
+
<span class="font-medium"><%= pagy.from %></span>
|
|
8
|
+
to
|
|
9
|
+
<span class="font-medium"><%= pagy.to %></span>
|
|
10
|
+
of
|
|
11
|
+
<span class="font-medium"><%= pagy.count %></span>
|
|
12
|
+
results
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
<nav class="flex items-center space-x-1" aria-label="Pagination">
|
|
16
|
+
<% if pagy.previous %>
|
|
17
|
+
<%= link_to url_for(page: pagy.previous), class: "relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" do %>
|
|
18
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
19
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
20
|
+
</svg>
|
|
21
|
+
<span class="ml-1">Prev</span>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% else %>
|
|
24
|
+
<span class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-300 bg-white border border-gray-200 rounded-md cursor-not-allowed">
|
|
25
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
26
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
27
|
+
</svg>
|
|
28
|
+
<span class="ml-1">Prev</span>
|
|
29
|
+
</span>
|
|
30
|
+
<% end %>
|
|
31
|
+
|
|
32
|
+
<% (1..pagy.pages).each do |page_num| %>
|
|
33
|
+
<% if page_num == pagy.page %>
|
|
34
|
+
<span class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-white bg-indigo-600 border border-indigo-600 rounded-md"><%= page_num %></span>
|
|
35
|
+
<% elsif page_num == 1 || page_num == pagy.pages || (page_num >= pagy.page - 2 && page_num <= pagy.page + 2) %>
|
|
36
|
+
<%= link_to page_num, url_for(page: page_num), class: "relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" %>
|
|
37
|
+
<% elsif page_num == pagy.page - 3 || page_num == pagy.page + 3 %>
|
|
38
|
+
<span class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-700">...</span>
|
|
39
|
+
<% end %>
|
|
40
|
+
<% end %>
|
|
41
|
+
|
|
42
|
+
<% if pagy.next %>
|
|
43
|
+
<%= link_to url_for(page: pagy.next), class: "relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" do %>
|
|
44
|
+
<span class="mr-1">Next</span>
|
|
45
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
46
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
47
|
+
</svg>
|
|
48
|
+
<% end %>
|
|
49
|
+
<% else %>
|
|
50
|
+
<span class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-300 bg-white border border-gray-200 rounded-md cursor-not-allowed">
|
|
51
|
+
<span class="mr-1">Next</span>
|
|
52
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
53
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
54
|
+
</svg>
|
|
55
|
+
</span>
|
|
56
|
+
<% end %>
|
|
57
|
+
</nav>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<% end %>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
2
|
+
<div class="p-5">
|
|
3
|
+
<div class="flex items-center">
|
|
4
|
+
<div class="flex-shrink-0">
|
|
5
|
+
<% case icon %>
|
|
6
|
+
<% when "database" %>
|
|
7
|
+
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
8
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
|
9
|
+
</svg>
|
|
10
|
+
<% when "calendar" %>
|
|
11
|
+
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
12
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
13
|
+
</svg>
|
|
14
|
+
<% when "tag" %>
|
|
15
|
+
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
16
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
|
17
|
+
</svg>
|
|
18
|
+
<% when "clock" %>
|
|
19
|
+
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
20
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
21
|
+
</svg>
|
|
22
|
+
<% else %>
|
|
23
|
+
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
24
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
25
|
+
</svg>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="ml-5 w-0 flex-1">
|
|
29
|
+
<dl>
|
|
30
|
+
<dt class="text-sm font-medium text-gray-500 truncate"><%= title %></dt>
|
|
31
|
+
<dd class="text-lg font-semibold text-gray-900"><%= value %></dd>
|
|
32
|
+
</dl>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
RailsEventViewer::Engine.routes.draw do
|
|
2
|
+
root to: "dashboard#index"
|
|
3
|
+
|
|
4
|
+
resources :events, only: [:index, :show] do
|
|
5
|
+
collection do
|
|
6
|
+
get :search
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
namespace :analytics do
|
|
11
|
+
get :overview
|
|
12
|
+
get :events_over_time
|
|
13
|
+
get :events_by_type
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
resources :event_types, only: [:index, :show], param: :name
|
|
17
|
+
resources :groups, only: [:index, :show]
|
|
18
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class CreateRailsEventViewerEntries < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :rails_event_viewer_entries do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
|
|
6
|
+
# Use jsonb for PostgreSQL (better performance), json for others (SQLite, MySQL)
|
|
7
|
+
json_type = postgresql? ? :jsonb : :json
|
|
8
|
+
t.column :payload, json_type, default: {}
|
|
9
|
+
t.column :tags, json_type, default: {}
|
|
10
|
+
t.column :context, json_type, default: {}
|
|
11
|
+
|
|
12
|
+
t.string :source_file
|
|
13
|
+
t.integer :source_line
|
|
14
|
+
t.string :source_label
|
|
15
|
+
t.datetime :occurred_at, null: false
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :rails_event_viewer_entries, :name
|
|
21
|
+
add_index :rails_event_viewer_entries, :occurred_at
|
|
22
|
+
add_index :rails_event_viewer_entries, [:name, :occurred_at]
|
|
23
|
+
|
|
24
|
+
# GIN index for JSONB columns (PostgreSQL only)
|
|
25
|
+
if postgresql?
|
|
26
|
+
add_index :rails_event_viewer_entries, :tags, using: :gin
|
|
27
|
+
add_index :rails_event_viewer_entries, :context, using: :gin
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def postgresql?
|
|
34
|
+
connection.adapter_name.downcase.include?("postgresql")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Installs RailsEventViewer into your Rails application.
|
|
3
|
+
|
|
4
|
+
This will:
|
|
5
|
+
- Create a migration for the events table (if using ActiveRecord adapter)
|
|
6
|
+
- Create an initializer with configuration options
|
|
7
|
+
- Mount the engine in your routes file
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
bin/rails generate rails_event_viewer:install
|
|
11
|
+
|
|
12
|
+
This will create:
|
|
13
|
+
db/migrate/YYYYMMDDHHMMSS_create_rails_event_viewer_entries.rb
|
|
14
|
+
config/initializers/rails_event_viewer.rb
|
|
15
|
+
|
|
16
|
+
And add to config/routes.rb:
|
|
17
|
+
mount RailsEventViewer::Engine, at: "/events"
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--storage-adapter=ADAPTER # Storage adapter to use: active_record (default), redis, memory
|
|
21
|
+
--skip-migration # Skip creating the migration file
|
|
22
|
+
--skip-initializer # Skip creating the initializer
|
|
23
|
+
--skip-routes # Skip mounting the engine in routes
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
# Standard install with ActiveRecord
|
|
27
|
+
bin/rails generate rails_event_viewer:install
|
|
28
|
+
|
|
29
|
+
# Install with Redis adapter (no migration needed)
|
|
30
|
+
bin/rails generate rails_event_viewer:install --storage-adapter=redis
|
|
31
|
+
|
|
32
|
+
# Only create initializer (skip migration and routes)
|
|
33
|
+
bin/rails generate rails_event_viewer:install --skip-migration --skip-routes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module RailsEventViewer
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
include ActiveRecord::Generators::Migration
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Installs RailsEventViewer: creates migration, initializer, and mounts the engine"
|
|
12
|
+
|
|
13
|
+
class_option :storage_adapter, type: :string, default: "active_record",
|
|
14
|
+
desc: "Storage adapter to use (active_record, redis, memory)"
|
|
15
|
+
|
|
16
|
+
class_option :skip_migration, type: :boolean, default: false,
|
|
17
|
+
desc: "Skip creating the migration file"
|
|
18
|
+
|
|
19
|
+
class_option :skip_initializer, type: :boolean, default: false,
|
|
20
|
+
desc: "Skip creating the initializer file"
|
|
21
|
+
|
|
22
|
+
class_option :skip_routes, type: :boolean, default: false,
|
|
23
|
+
desc: "Skip mounting the engine in routes"
|
|
24
|
+
|
|
25
|
+
def create_migration_file
|
|
26
|
+
return if options[:skip_migration]
|
|
27
|
+
return if options[:storage_adapter] != "active_record"
|
|
28
|
+
|
|
29
|
+
migration_template(
|
|
30
|
+
"create_rails_event_viewer_entries.rb.erb",
|
|
31
|
+
"db/migrate/create_rails_event_viewer_entries.rb"
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create_initializer
|
|
36
|
+
return if options[:skip_initializer]
|
|
37
|
+
|
|
38
|
+
template(
|
|
39
|
+
"initializer.rb.erb",
|
|
40
|
+
"config/initializers/rails_event_viewer.rb"
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def mount_engine
|
|
45
|
+
return if options[:skip_routes]
|
|
46
|
+
|
|
47
|
+
route_line = 'mount RailsEventViewer::Engine, at: "/events"'
|
|
48
|
+
|
|
49
|
+
routes_file = File.join(destination_root, "config/routes.rb")
|
|
50
|
+
if File.exist?(routes_file) && File.read(routes_file).include?("RailsEventViewer::Engine")
|
|
51
|
+
say_status :skip, "Engine already mounted in routes", :yellow
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
route route_line
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def show_post_install_message
|
|
59
|
+
say ""
|
|
60
|
+
say "RailsEventViewer has been installed!", :green
|
|
61
|
+
say ""
|
|
62
|
+
say "Next steps:"
|
|
63
|
+
|
|
64
|
+
if options[:storage_adapter] == "active_record" && !options[:skip_migration]
|
|
65
|
+
say " 1. Run migrations: rails db:migrate"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
say " 2. Configure authentication in config/initializers/rails_event_viewer.rb"
|
|
69
|
+
say " 3. Visit /events to view your events"
|
|
70
|
+
say ""
|
|
71
|
+
say "For more information, visit: https://github.com/keshavbiswa/rails_event_viewer"
|
|
72
|
+
say ""
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def migration_version
|
|
78
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def storage_adapter
|
|
82
|
+
options[:storage_adapter]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/generators/rails_event_viewer/install/templates/create_rails_event_viewer_entries.rb.erb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class CreateRailsEventViewerEntries < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :rails_event_viewer_entries do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
|
|
6
|
+
json_type = postgresql? ? :jsonb : :json
|
|
7
|
+
t.column :payload, json_type, default: {}
|
|
8
|
+
t.column :tags, json_type, default: {}
|
|
9
|
+
t.column :context, json_type, default: {}
|
|
10
|
+
|
|
11
|
+
t.string :source_file
|
|
12
|
+
t.integer :source_line
|
|
13
|
+
t.string :source_label
|
|
14
|
+
t.datetime :occurred_at, null: false
|
|
15
|
+
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :rails_event_viewer_entries, :name
|
|
20
|
+
add_index :rails_event_viewer_entries, :occurred_at
|
|
21
|
+
add_index :rails_event_viewer_entries, [:name, :occurred_at]
|
|
22
|
+
|
|
23
|
+
# GIN index for JSONB columns (PostgreSQL only)
|
|
24
|
+
if postgresql?
|
|
25
|
+
add_index :rails_event_viewer_entries, :tags, using: :gin
|
|
26
|
+
add_index :rails_event_viewer_entries, :context, using: :gin
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def postgresql?
|
|
33
|
+
connection.adapter_name.downcase.include?("postgresql")
|
|
34
|
+
end
|
|
35
|
+
end
|