pg_sql_triggers 1.2.0 → 1.3.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 +4 -4
- data/CHANGELOG.md +144 -0
- data/COVERAGE.md +26 -19
- data/Goal.md +276 -155
- data/README.md +27 -1
- data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +50 -0
- data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +56 -0
- data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +66 -0
- data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +117 -0
- data/app/controllers/pg_sql_triggers/application_controller.rb +10 -62
- data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +102 -0
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +4 -9
- data/app/controllers/pg_sql_triggers/tables_controller.rb +30 -4
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +3 -21
- data/app/helpers/pg_sql_triggers/permissions_helper.rb +43 -0
- data/app/models/pg_sql_triggers/audit_log.rb +106 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +178 -9
- data/app/views/layouts/pg_sql_triggers/application.html.erb +26 -6
- data/app/views/pg_sql_triggers/audit_logs/index.html.erb +177 -0
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +33 -8
- data/app/views/pg_sql_triggers/tables/index.html.erb +76 -3
- data/app/views/pg_sql_triggers/tables/show.html.erb +17 -4
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +16 -7
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +16 -7
- data/app/views/pg_sql_triggers/triggers/show.html.erb +26 -6
- data/config/routes.rb +2 -0
- data/db/migrate/20260103000001_create_pg_sql_triggers_audit_log.rb +28 -0
- data/docs/README.md +15 -5
- data/docs/api-reference.md +191 -0
- data/docs/audit-trail.md +413 -0
- data/docs/configuration.md +6 -6
- data/docs/permissions.md +369 -0
- data/docs/troubleshooting.md +486 -0
- data/docs/ui-guide.md +211 -0
- data/docs/web-ui.md +257 -34
- data/lib/pg_sql_triggers/errors.rb +245 -0
- data/lib/pg_sql_triggers/generator/service.rb +32 -0
- data/lib/pg_sql_triggers/permissions/checker.rb +9 -2
- data/lib/pg_sql_triggers/registry.rb +141 -8
- data/lib/pg_sql_triggers/sql/kill_switch.rb +33 -5
- data/lib/pg_sql_triggers/testing/function_tester.rb +2 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +3 -6
- metadata +29 -6
- data/docs/screenshots/.gitkeep +0 -1
- data/docs/screenshots/Generate Trigger.png +0 -0
- data/docs/screenshots/Triggers Page.png +0 -0
- data/docs/screenshots/kill error.png +0 -0
- data/docs/screenshots/kill modal for migration down.png +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<%# Audit Log Index Page %>
|
|
2
|
+
<div style="max-width: 1400px; margin: 0 auto; padding: 2rem;">
|
|
3
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
4
|
+
<h1 style="margin: 0;">Audit Log</h1>
|
|
5
|
+
<div style="display: flex; gap: 1rem;">
|
|
6
|
+
<%= link_to "Dashboard", dashboard_path, style: "padding: 0.5rem 1rem; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
|
|
7
|
+
<% csv_params = params.permit(:trigger_name, :operation, :status, :environment, :actor_id).to_h %>
|
|
8
|
+
<%= link_to "Export CSV", audit_logs_path(csv_params.merge(format: :csv)), style: "padding: 0.5rem 1rem; background: #28a745; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<!-- Filters -->
|
|
13
|
+
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
|
|
14
|
+
<h3 style="margin-top: 0; margin-bottom: 1rem;">Filters</h3>
|
|
15
|
+
<%= form_with url: audit_logs_path, method: :get, local: true, style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;" do |f| %>
|
|
16
|
+
<div>
|
|
17
|
+
<%= f.label :trigger_name, "Trigger Name", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
18
|
+
<%= f.select :trigger_name, options_for_select([["All", ""]] + @available_trigger_names.map { |n| [n, n] }, params[:trigger_name]), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div>
|
|
22
|
+
<%= f.label :operation, "Operation", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
23
|
+
<%= f.select :operation, options_for_select([["All", ""]] + @available_operations.map { |o| [o.humanize, o] }, params[:operation]), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div>
|
|
27
|
+
<%= f.label :status, "Status", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
28
|
+
<%= f.select :status, options_for_select([["All", ""], ["Success", "success"], ["Failure", "failure"]], params[:status]), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div>
|
|
32
|
+
<%= f.label :environment, "Environment", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
33
|
+
<%= f.select :environment, options_for_select([["All", ""]] + @available_environments.map { |e| [e.humanize, e] }, params[:environment]), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div>
|
|
37
|
+
<%= f.label :sort, "Sort Order", style: "display: block; margin-bottom: 0.5rem; font-weight: 600;" %>
|
|
38
|
+
<%= f.select :sort, options_for_select([["Newest First", "desc"], ["Oldest First", "asc"]], params[:sort] || "desc"), {}, { style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" } %>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div style="display: flex; align-items: flex-end; gap: 0.5rem;">
|
|
42
|
+
<%= f.submit "Apply Filters", style: "padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;" %>
|
|
43
|
+
<%= link_to "Clear", audit_logs_path, style: "padding: 0.5rem 1rem; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- Results Summary -->
|
|
49
|
+
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
|
|
50
|
+
<strong>Total Results:</strong> <%= @total_count %> entries
|
|
51
|
+
<% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? %>
|
|
52
|
+
<span style="color: #6c757d;">(filtered)</span>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Audit Log Table -->
|
|
57
|
+
<% if @audit_logs.any? %>
|
|
58
|
+
<div style="background: white; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow-x: auto;">
|
|
59
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
60
|
+
<thead>
|
|
61
|
+
<tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;">
|
|
62
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Time</th>
|
|
63
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Trigger</th>
|
|
64
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Operation</th>
|
|
65
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Status</th>
|
|
66
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Environment</th>
|
|
67
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Actor</th>
|
|
68
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Reason</th>
|
|
69
|
+
<th style="padding: 0.75rem; text-align: left; font-weight: 600;">Error</th>
|
|
70
|
+
</tr>
|
|
71
|
+
</thead>
|
|
72
|
+
<tbody>
|
|
73
|
+
<% @audit_logs.each do |log| %>
|
|
74
|
+
<tr style="border-bottom: 1px solid #dee2e6;">
|
|
75
|
+
<td style="padding: 0.75rem;">
|
|
76
|
+
<span title="<%= log.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>" style="cursor: help;">
|
|
77
|
+
<%= distance_of_time_in_words_to_now(log.created_at) %> ago
|
|
78
|
+
</span>
|
|
79
|
+
<br>
|
|
80
|
+
<small style="color: #6c757d;"><%= log.created_at.strftime("%Y-%m-%d %H:%M:%S") %></small>
|
|
81
|
+
</td>
|
|
82
|
+
<td style="padding: 0.75rem;">
|
|
83
|
+
<% if log.trigger_name.present? %>
|
|
84
|
+
<%= link_to log.trigger_name, trigger_path(PgSqlTriggers::TriggerRegistry.find_by(trigger_name: log.trigger_name)), style: "color: #007bff; text-decoration: none;" rescue log.trigger_name %>
|
|
85
|
+
<% else %>
|
|
86
|
+
<span style="color: #6c757d;">—</span>
|
|
87
|
+
<% end %>
|
|
88
|
+
</td>
|
|
89
|
+
<td style="padding: 0.75rem;">
|
|
90
|
+
<code style="background: #f8f9fa; padding: 0.25rem 0.5rem; border-radius: 3px; font-size: 0.875rem;"><%= log.operation %></code>
|
|
91
|
+
</td>
|
|
92
|
+
<td style="padding: 0.75rem;">
|
|
93
|
+
<% if log.status == "success" %>
|
|
94
|
+
<span style="background: #d4edda; color: #155724; padding: 0.25rem 0.5rem; border-radius: 3px; font-size: 0.875rem; font-weight: 600;">Success</span>
|
|
95
|
+
<% else %>
|
|
96
|
+
<span style="background: #f8d7da; color: #721c24; padding: 0.25rem 0.5rem; border-radius: 3px; font-size: 0.875rem; font-weight: 600;">Failure</span>
|
|
97
|
+
<% end %>
|
|
98
|
+
</td>
|
|
99
|
+
<td style="padding: 0.75rem;">
|
|
100
|
+
<% if log.environment.present? %>
|
|
101
|
+
<span class="badge badge-info"><%= log.environment %></span>
|
|
102
|
+
<% else %>
|
|
103
|
+
<span style="color: #6c757d;">—</span>
|
|
104
|
+
<% end %>
|
|
105
|
+
</td>
|
|
106
|
+
<td style="padding: 0.75rem;">
|
|
107
|
+
<% if log.actor.present? %>
|
|
108
|
+
<% actor_type = log.actor.is_a?(Hash) ? (log.actor["type"] || log.actor[:type]) : nil %>
|
|
109
|
+
<% actor_id = log.actor.is_a?(Hash) ? (log.actor["id"] || log.actor[:id]) : nil %>
|
|
110
|
+
<% if actor_type && actor_id %>
|
|
111
|
+
<code style="background: #f8f9fa; padding: 0.25rem 0.5rem; border-radius: 3px; font-size: 0.875rem;"><%= "#{actor_type}:#{actor_id}" %></code>
|
|
112
|
+
<% else %>
|
|
113
|
+
<span style="color: #6c757d;">—</span>
|
|
114
|
+
<% end %>
|
|
115
|
+
<% else %>
|
|
116
|
+
<span style="color: #6c757d;">—</span>
|
|
117
|
+
<% end %>
|
|
118
|
+
</td>
|
|
119
|
+
<td style="padding: 0.75rem;">
|
|
120
|
+
<% if log.reason.present? %>
|
|
121
|
+
<span title="<%= log.reason %>" style="cursor: help;">
|
|
122
|
+
<%= truncate(log.reason, length: 50) %>
|
|
123
|
+
</span>
|
|
124
|
+
<% else %>
|
|
125
|
+
<span style="color: #6c757d;">—</span>
|
|
126
|
+
<% end %>
|
|
127
|
+
</td>
|
|
128
|
+
<td style="padding: 0.75rem;">
|
|
129
|
+
<% if log.error_message.present? %>
|
|
130
|
+
<span style="color: #dc3545; cursor: help;" title="<%= log.error_message %>">
|
|
131
|
+
<%= truncate(log.error_message, length: 50) %>
|
|
132
|
+
</span>
|
|
133
|
+
<% else %>
|
|
134
|
+
<span style="color: #6c757d;">—</span>
|
|
135
|
+
<% end %>
|
|
136
|
+
</td>
|
|
137
|
+
</tr>
|
|
138
|
+
<% end %>
|
|
139
|
+
</tbody>
|
|
140
|
+
</table>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Pagination -->
|
|
144
|
+
<% if @total_pages > 1 %>
|
|
145
|
+
<div style="display: flex; justify-content: center; align-items: center; gap: 1rem; margin-top: 2rem;">
|
|
146
|
+
<% if @page > 1 %>
|
|
147
|
+
<% prev_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page).to_h %>
|
|
148
|
+
<%= link_to "« Previous", audit_logs_path(prev_params.merge(page: @page - 1)), style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px;" %>
|
|
149
|
+
<% else %>
|
|
150
|
+
<span style="padding: 0.5rem 1rem; background: #e9ecef; color: #6c757d; border-radius: 4px; cursor: not-allowed;">« Previous</span>
|
|
151
|
+
<% end %>
|
|
152
|
+
|
|
153
|
+
<span style="color: #6c757d;">
|
|
154
|
+
Page <%= @page %> of <%= @total_pages %>
|
|
155
|
+
</span>
|
|
156
|
+
|
|
157
|
+
<% if @page < @total_pages %>
|
|
158
|
+
<% next_params = params.except(:page).permit(:trigger_name, :operation, :status, :environment, :sort, :per_page).to_h %>
|
|
159
|
+
<%= link_to "Next »", audit_logs_path(next_params.merge(page: @page + 1)), style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px;" %>
|
|
160
|
+
<% else %>
|
|
161
|
+
<span style="padding: 0.5rem 1rem; background: #e9ecef; color: #6c757d; border-radius: 4px; cursor: not-allowed;">Next »</span>
|
|
162
|
+
<% end %>
|
|
163
|
+
</div>
|
|
164
|
+
<% end %>
|
|
165
|
+
<% else %>
|
|
166
|
+
<div style="background: white; padding: 3rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center;">
|
|
167
|
+
<h3 style="margin-top: 0; color: #6c757d;">No audit log entries found</h3>
|
|
168
|
+
<p style="color: #6c757d;">
|
|
169
|
+
<% if params[:trigger_name].present? || params[:operation].present? || params[:status].present? || params[:environment].present? %>
|
|
170
|
+
Try adjusting your filters or <%= link_to "clear filters", audit_logs_path, style: "color: #007bff;" %>.
|
|
171
|
+
<% else %>
|
|
172
|
+
Audit log entries will appear here as operations are performed.
|
|
173
|
+
<% end %>
|
|
174
|
+
</p>
|
|
175
|
+
</div>
|
|
176
|
+
<% end %>
|
|
177
|
+
</div>
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
<th>Version</th>
|
|
41
41
|
<th>Status</th>
|
|
42
42
|
<th>Source</th>
|
|
43
|
+
<th>Last Applied</th>
|
|
43
44
|
<th>Actions</th>
|
|
44
45
|
</tr>
|
|
45
46
|
</thead>
|
|
@@ -60,11 +61,20 @@
|
|
|
60
61
|
</td>
|
|
61
62
|
<td><span class="badge badge-info"><%= trigger.source %></span></td>
|
|
62
63
|
<td>
|
|
63
|
-
<% if
|
|
64
|
-
<
|
|
64
|
+
<% if trigger.installed_at.present? %>
|
|
65
|
+
<span title="<%= trigger.installed_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>" style="cursor: help;">
|
|
66
|
+
<%= distance_of_time_in_words_to_now(trigger.installed_at) %> ago
|
|
67
|
+
</span>
|
|
68
|
+
<% else %>
|
|
69
|
+
<span style="color: #6c757d;">Never</span>
|
|
70
|
+
<% end %>
|
|
71
|
+
</td>
|
|
72
|
+
<td>
|
|
73
|
+
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
74
|
+
<% if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger) %>
|
|
65
75
|
<% if trigger.enabled %>
|
|
66
76
|
<% form_id = "trigger-disable-#{trigger.id}-form" %>
|
|
67
|
-
<%= form_with url: disable_trigger_path(trigger), method: :post, local:
|
|
77
|
+
<%= form_with url: disable_trigger_path(trigger), method: :post, local: false, id: form_id, style: "margin: 0;" do |f| %>
|
|
68
78
|
<%= f.hidden_field :redirect_to, value: dashboard_path %>
|
|
69
79
|
<button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
|
|
70
80
|
style="padding: 0.25rem 0.75rem; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
@@ -78,7 +88,7 @@
|
|
|
78
88
|
message: "Are you sure you want to disable trigger '#{trigger.trigger_name}'?" %>
|
|
79
89
|
<% else %>
|
|
80
90
|
<% form_id = "trigger-enable-#{trigger.id}-form" %>
|
|
81
|
-
<%= form_with url: enable_trigger_path(trigger), method: :post, local:
|
|
91
|
+
<%= form_with url: enable_trigger_path(trigger), method: :post, local: false, id: form_id, style: "margin: 0;" do |f| %>
|
|
82
92
|
<%= f.hidden_field :redirect_to, value: dashboard_path %>
|
|
83
93
|
<button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
|
|
84
94
|
style="padding: 0.25rem 0.75rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
@@ -91,10 +101,25 @@
|
|
|
91
101
|
title: 'Enable Trigger',
|
|
92
102
|
message: "Are you sure you want to enable trigger '#{trigger.trigger_name}'?" %>
|
|
93
103
|
<% end %>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
<% end %>
|
|
105
|
+
|
|
106
|
+
<% if PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger) %>
|
|
107
|
+
<% begin %>
|
|
108
|
+
<% drift_info = trigger.drift_result %>
|
|
109
|
+
<% if drift_info && drift_info[:state] == 'drifted' %>
|
|
110
|
+
<%= render 'pg_sql_triggers/triggers/re_execute_modal', trigger: trigger, drift_info: drift_info, redirect_to: dashboard_path, button_size: :small %>
|
|
111
|
+
<% end %>
|
|
112
|
+
<% rescue StandardError %>
|
|
113
|
+
<%# Skip if drift detection fails %>
|
|
114
|
+
<% end %>
|
|
115
|
+
|
|
116
|
+
<%= render 'pg_sql_triggers/triggers/drop_modal', trigger: trigger, redirect_to: dashboard_path, button_size: :small %>
|
|
117
|
+
<% end %>
|
|
118
|
+
|
|
119
|
+
<% unless PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger) || PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger) %>
|
|
120
|
+
<span style="color: #6c757d;">—</span>
|
|
121
|
+
<% end %>
|
|
122
|
+
</div>
|
|
98
123
|
</td>
|
|
99
124
|
</tr>
|
|
100
125
|
<% end %>
|
|
@@ -9,11 +9,37 @@
|
|
|
9
9
|
<div style="font-size: 2rem; font-weight: 600; color: #28a745;"><%= @tables_with_trigger_count %></div>
|
|
10
10
|
<div style="color: #6c757d;">Tables with Triggers</div>
|
|
11
11
|
</div>
|
|
12
|
+
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
|
13
|
+
<div style="font-size: 2rem; font-weight: 600; color: #6c757d;"><%= @tables_without_trigger_count %></div>
|
|
14
|
+
<div style="color: #6c757d;">Tables without Triggers</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
|
17
|
+
<div style="font-size: 2rem; font-weight: 600; color: #007bff;"><%= @total_tables_count %></div>
|
|
18
|
+
<div style="color: #6c757d;">Total Tables</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Filter Controls -->
|
|
23
|
+
<div style="background: white; padding: 1rem 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
|
|
24
|
+
<div style="display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
|
25
|
+
<label style="color: #495057; font-weight: 600; font-size: 0.875rem;">Filter:</label>
|
|
26
|
+
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
27
|
+
<%= link_to "All Tables", tables_path(filter: 'all', page: 1, per_page: @per_page),
|
|
28
|
+
style: "padding: 0.5rem 1rem; background: #{@filter == 'all' ? '#007bff' : '#f8f9fa'}; color: #{@filter == 'all' ? 'white' : '#495057'}; text-decoration: none; border-radius: 4px; border: 1px solid #{@filter == 'all' ? '#007bff' : '#dee2e6'}; font-size: 0.875rem; font-weight: #{@filter == 'all' ? '600' : '400'};" %>
|
|
29
|
+
<%= link_to "With Triggers", tables_path(filter: 'with_triggers', page: 1, per_page: @per_page),
|
|
30
|
+
style: "padding: 0.5rem 1rem; background: #{@filter == 'with_triggers' ? '#28a745' : '#f8f9fa'}; color: #{@filter == 'with_triggers' ? 'white' : '#495057'}; text-decoration: none; border-radius: 4px; border: 1px solid #{@filter == 'with_triggers' ? '#28a745' : '#dee2e6'}; font-size: 0.875rem; font-weight: #{@filter == 'with_triggers' ? '600' : '400'};" %>
|
|
31
|
+
<%= link_to "Without Triggers", tables_path(filter: 'without_triggers', page: 1, per_page: @per_page),
|
|
32
|
+
style: "padding: 0.5rem 1rem; background: #{@filter == 'without_triggers' ? '#6c757d' : '#f8f9fa'}; color: #{@filter == 'without_triggers' ? 'white' : '#495057'}; text-decoration: none; border-radius: 4px; border: 1px solid #{@filter == 'without_triggers' ? '#6c757d' : '#dee2e6'}; font-size: 0.875rem; font-weight: #{@filter == 'without_triggers' ? '600' : '400'};" %>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
12
35
|
</div>
|
|
13
36
|
|
|
14
37
|
<!-- Tables List -->
|
|
15
38
|
<% if @tables_with_triggers.any? %>
|
|
16
39
|
<div style="background: white; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden;">
|
|
40
|
+
<div style="margin-bottom: 1rem; color: #6c757d; font-size: 0.875rem; padding: 1rem 1rem 0 1rem;">
|
|
41
|
+
Showing <%= (@page - 1) * @per_page + 1 %>-<%= [@page * @per_page, @total_tables].min %> of <%= @total_tables %> tables
|
|
42
|
+
</div>
|
|
17
43
|
<table style="margin: 0;">
|
|
18
44
|
<thead>
|
|
19
45
|
<tr>
|
|
@@ -93,11 +119,58 @@
|
|
|
93
119
|
<% end %>
|
|
94
120
|
</tbody>
|
|
95
121
|
</table>
|
|
122
|
+
|
|
123
|
+
<!-- Pagination Controls -->
|
|
124
|
+
<% if @total_pages > 1 %>
|
|
125
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; padding: 1rem; border-top: 1px solid #dee2e6;">
|
|
126
|
+
<div>
|
|
127
|
+
<% if @page > 1 %>
|
|
128
|
+
<%= link_to "← Previous", tables_path(filter: @filter, page: @page - 1, per_page: @per_page),
|
|
129
|
+
style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px; margin-right: 0.5rem;" %>
|
|
130
|
+
<% end %>
|
|
131
|
+
<% if @page < @total_pages %>
|
|
132
|
+
<%= link_to "Next →", tables_path(filter: @filter, page: @page + 1, per_page: @per_page),
|
|
133
|
+
style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px;" %>
|
|
134
|
+
<% end %>
|
|
135
|
+
</div>
|
|
136
|
+
<div style="color: #6c757d; font-size: 0.875rem;">
|
|
137
|
+
Page <%= @page %> of <%= @total_pages %>
|
|
138
|
+
</div>
|
|
139
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
140
|
+
<label style="color: #6c757d; font-size: 0.875rem;">Per page:</label>
|
|
141
|
+
<select onchange="window.location.href='<%= tables_path %>?filter=<%= @filter %>&page=1&per_page=' + this.value"
|
|
142
|
+
style="padding: 0.25rem 0.5rem; border: 1px solid #ced4da; border-radius: 4px;">
|
|
143
|
+
<option value="10" <%= 'selected' if @per_page == 10 %>>10</option>
|
|
144
|
+
<option value="20" <%= 'selected' if @per_page == 20 %>>20</option>
|
|
145
|
+
<option value="50" <%= 'selected' if @per_page == 50 %>>50</option>
|
|
146
|
+
<option value="100" <%= 'selected' if @per_page == 100 %>>100</option>
|
|
147
|
+
</select>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
<% end %>
|
|
96
151
|
</div>
|
|
97
152
|
<% else %>
|
|
98
153
|
<div style="background: #e7f3ff; border-left: 4px solid #007bff; padding: 1.5rem; border-radius: 4px;">
|
|
99
|
-
<h3 style="margin-top: 0;">
|
|
100
|
-
|
|
101
|
-
|
|
154
|
+
<h3 style="margin-top: 0;">
|
|
155
|
+
<% if @filter == 'with_triggers' %>
|
|
156
|
+
No tables with triggers found
|
|
157
|
+
<% elsif @filter == 'without_triggers' %>
|
|
158
|
+
No tables without triggers found
|
|
159
|
+
<% else %>
|
|
160
|
+
No tables found
|
|
161
|
+
<% end %>
|
|
162
|
+
</h3>
|
|
163
|
+
<p style="margin-bottom: 1rem;">
|
|
164
|
+
<% if @filter == 'with_triggers' %>
|
|
165
|
+
No tables with triggers were found in the database. Create your first trigger to get started.
|
|
166
|
+
<% elsif @filter == 'without_triggers' %>
|
|
167
|
+
All tables in the database have triggers.
|
|
168
|
+
<% else %>
|
|
169
|
+
No tables were found in the database.
|
|
170
|
+
<% end %>
|
|
171
|
+
</p>
|
|
172
|
+
<% if @filter == 'with_triggers' || @filter == 'all' %>
|
|
173
|
+
<%= link_to "Generate New Trigger", new_generator_path, class: "btn btn-primary" %>
|
|
174
|
+
<% end %>
|
|
102
175
|
</div>
|
|
103
176
|
<% end %>
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
<div style="margin-bottom: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
68
|
+
<% if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger) %>
|
|
69
69
|
<% if trigger.enabled %>
|
|
70
70
|
<% form_id = "table-trigger-disable-#{trigger.id}-form" %>
|
|
71
71
|
<%= form_with url: disable_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
|
|
@@ -95,8 +95,21 @@
|
|
|
95
95
|
title: 'Enable Trigger',
|
|
96
96
|
message: "Are you sure you want to enable trigger '#{trigger.trigger_name}' on table '#{@table_info[:table_name]}'?" %>
|
|
97
97
|
<% end %>
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
<% end %>
|
|
99
|
+
|
|
100
|
+
<% if PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger) %>
|
|
101
|
+
<% begin %>
|
|
102
|
+
<% drift_info = trigger.drift_result %>
|
|
103
|
+
<% if drift_info && drift_info[:state] == 'drifted' %>
|
|
104
|
+
<%= render 'pg_sql_triggers/triggers/re_execute_modal', trigger: trigger, drift_info: drift_info, redirect_to: table_path(@table_info[:table_name]) %>
|
|
105
|
+
<% end %>
|
|
106
|
+
<% rescue StandardError %>
|
|
107
|
+
<%# Skip if drift detection fails %>
|
|
108
|
+
<% end %>
|
|
109
|
+
|
|
110
|
+
<%= render 'pg_sql_triggers/triggers/drop_modal', trigger: trigger, redirect_to: table_path(@table_info[:table_name]) %>
|
|
111
|
+
<% end %>
|
|
112
|
+
</div>
|
|
100
113
|
|
|
101
114
|
<% if trigger.definition.present? %>
|
|
102
115
|
<% definition = JSON.parse(trigger.definition) rescue {} %>
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
<% form_id = "trigger-drop-#{trigger.id}-form" %>
|
|
1
|
+
<% form_id = local_assigns[:form_id] || "trigger-drop-#{trigger.id}-form" %>
|
|
2
|
+
<% redirect_path = local_assigns[:redirect_to] || params[:redirect_to] || trigger_path(trigger) %>
|
|
3
|
+
<% button_size = local_assigns[:button_size] || :medium %>
|
|
2
4
|
|
|
3
5
|
<%= form_with url: drop_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0; display: inline-block;" do |f| %>
|
|
4
|
-
<%= f.hidden_field :redirect_to, value:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
<%= f.hidden_field :redirect_to, value: redirect_path %>
|
|
7
|
+
|
|
8
|
+
<% if button_size == :small %>
|
|
9
|
+
<button type="button" onclick="showDropModal('<%= form_id %>')"
|
|
10
|
+
style="padding: 0.25rem 0.75rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
11
|
+
Drop
|
|
12
|
+
</button>
|
|
13
|
+
<% else %>
|
|
14
|
+
<button type="button" onclick="showDropModal('<%= form_id %>')"
|
|
15
|
+
style="padding: 0.75rem 1.5rem; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
|
|
16
|
+
Drop Trigger
|
|
17
|
+
</button>
|
|
18
|
+
<% end %>
|
|
10
19
|
<% end %>
|
|
11
20
|
|
|
12
21
|
<!-- Drop Modal -->
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
<% form_id = "trigger-re-execute-#{trigger.id}-form" %>
|
|
1
|
+
<% form_id = local_assigns[:form_id] || "trigger-re-execute-#{trigger.id}-form" %>
|
|
2
|
+
<% redirect_path = local_assigns[:redirect_to] || params[:redirect_to] || trigger_path(trigger) %>
|
|
3
|
+
<% button_size = local_assigns[:button_size] || :medium %>
|
|
2
4
|
|
|
3
5
|
<%= form_with url: re_execute_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0; display: inline-block;" do |f| %>
|
|
4
|
-
<%= f.hidden_field :redirect_to, value:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
<%= f.hidden_field :redirect_to, value: redirect_path %>
|
|
7
|
+
|
|
8
|
+
<% if button_size == :small %>
|
|
9
|
+
<button type="button" onclick="showReExecuteModal('<%= form_id %>')"
|
|
10
|
+
style="padding: 0.25rem 0.75rem; background: #ffc107; color: #000; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
11
|
+
Re-Execute
|
|
12
|
+
</button>
|
|
13
|
+
<% else %>
|
|
14
|
+
<button type="button" onclick="showReExecuteModal('<%= form_id %>')"
|
|
15
|
+
style="padding: 0.75rem 1.5rem; background: #ffc107; color: #000; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
|
|
16
|
+
Re-Execute Trigger
|
|
17
|
+
</button>
|
|
18
|
+
<% end %>
|
|
10
19
|
<% end %>
|
|
11
20
|
|
|
12
21
|
<!-- Re-Execute Modal -->
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
<%= link_to "
|
|
4
|
-
<%= link_to "
|
|
1
|
+
<!-- Breadcrumb Navigation -->
|
|
2
|
+
<div style="margin-bottom: 1rem; color: #6c757d; font-size: 0.875rem;">
|
|
3
|
+
<%= link_to "Dashboard", dashboard_path, style: "color: #007bff; text-decoration: none;" %> /
|
|
4
|
+
<%= link_to "Tables", tables_path, style: "color: #007bff; text-decoration: none;" %> /
|
|
5
|
+
<%= link_to @trigger.table_name, table_path(@trigger.table_name), style: "color: #007bff; text-decoration: none;" %> /
|
|
6
|
+
<span style="color: #495057; font-weight: 600;"><%= @trigger.trigger_name %></span>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
10
|
+
<h2 style="margin: 0;">Trigger Details: <%= @trigger.trigger_name %></h2>
|
|
11
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
12
|
+
<%= link_to "← Back to Dashboard", dashboard_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none; padding: 0.5rem 1rem;" %>
|
|
13
|
+
<%= link_to "View Table", table_path(@trigger.table_name), class: "btn", style: "background: #007bff; color: white; text-decoration: none; padding: 0.5rem 1rem;" %>
|
|
14
|
+
</div>
|
|
5
15
|
</div>
|
|
6
16
|
|
|
7
17
|
<!-- Drift Warning -->
|
|
@@ -55,8 +65,18 @@
|
|
|
55
65
|
|
|
56
66
|
<% if @trigger.installed_at.present? %>
|
|
57
67
|
<div style="margin-bottom: 1rem;">
|
|
58
|
-
<div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">
|
|
59
|
-
<div
|
|
68
|
+
<div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">Last Applied</div>
|
|
69
|
+
<div title="<%= @trigger.installed_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>">
|
|
70
|
+
<%= distance_of_time_in_words_to_now(@trigger.installed_at) %> ago
|
|
71
|
+
<small style="color: #6c757d;">(<%= @trigger.installed_at.strftime("%Y-%m-%d %H:%M:%S") %>)</small>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<% end %>
|
|
75
|
+
|
|
76
|
+
<% if @trigger.last_verified_at.present? %>
|
|
77
|
+
<div style="margin-bottom: 1rem;">
|
|
78
|
+
<div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">Last Verified</div>
|
|
79
|
+
<div><%= @trigger.last_verified_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
|
|
60
80
|
</div>
|
|
61
81
|
<% end %>
|
|
62
82
|
|
data/config/routes.rb
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreatePgSqlTriggersAuditLog < ActiveRecord::Migration[6.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :pg_sql_triggers_audit_log do |t|
|
|
6
|
+
t.string :trigger_name
|
|
7
|
+
t.string :operation, null: false
|
|
8
|
+
t.jsonb :actor # Store actor information (type, id)
|
|
9
|
+
t.string :environment
|
|
10
|
+
t.string :status, null: false # success, failure
|
|
11
|
+
t.text :reason
|
|
12
|
+
t.string :confirmation_text
|
|
13
|
+
t.jsonb :before_state # Store state before operation
|
|
14
|
+
t.jsonb :after_state # Store state after operation
|
|
15
|
+
t.text :diff # Store diff if applicable
|
|
16
|
+
t.text :error_message # Store error message if operation failed
|
|
17
|
+
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
add_index :pg_sql_triggers_audit_log, :trigger_name
|
|
22
|
+
add_index :pg_sql_triggers_audit_log, :operation
|
|
23
|
+
add_index :pg_sql_triggers_audit_log, :status
|
|
24
|
+
add_index :pg_sql_triggers_audit_log, :environment
|
|
25
|
+
add_index :pg_sql_triggers_audit_log, :created_at
|
|
26
|
+
add_index :pg_sql_triggers_audit_log, %i[trigger_name created_at]
|
|
27
|
+
end
|
|
28
|
+
end
|
data/docs/README.md
CHANGED
|
@@ -9,12 +9,16 @@ Welcome to the PgSqlTriggers documentation. This directory contains comprehensiv
|
|
|
9
9
|
|
|
10
10
|
### Core Guides
|
|
11
11
|
- **[Usage Guide](usage-guide.md)** - DSL syntax, migrations, and drift detection
|
|
12
|
-
- **[
|
|
12
|
+
- **[UI Guide](ui-guide.md)** - Quick start guide for the web interface
|
|
13
|
+
- **[Web UI Guide](web-ui.md)** - Comprehensive web dashboard documentation
|
|
13
14
|
- **[Kill Switch Guide](kill-switch.md)** - Production safety features
|
|
15
|
+
- **[Permissions Guide](permissions.md)** - Configuring and using permissions
|
|
16
|
+
- **[Audit Trail Guide](audit-trail.md)** - Viewing and exporting audit logs
|
|
14
17
|
|
|
15
18
|
### Reference
|
|
16
19
|
- **[Configuration Reference](configuration.md)** - Complete configuration options
|
|
17
20
|
- **[API Reference](api-reference.md)** - Console API and programmatic usage
|
|
21
|
+
- **[Troubleshooting Guide](troubleshooting.md)** - Common issues and solutions
|
|
18
22
|
|
|
19
23
|
## Quick Links
|
|
20
24
|
|
|
@@ -30,20 +34,26 @@ See [Usage Guide - Declaring Triggers](usage-guide.md#declaring-triggers)
|
|
|
30
34
|
See [Usage Guide - Trigger Migrations](usage-guide.md#trigger-migrations)
|
|
31
35
|
|
|
32
36
|
#### Use the web dashboard
|
|
33
|
-
|
|
37
|
+
Start with [UI Guide](ui-guide.md) or see [Web UI Documentation](web-ui.md) for comprehensive details
|
|
38
|
+
|
|
39
|
+
#### Configure permissions
|
|
40
|
+
See [Permissions Guide](permissions.md) or [Configuration - Permission System](configuration.md#permission-system)
|
|
41
|
+
|
|
42
|
+
#### View audit logs
|
|
43
|
+
See [Audit Trail Guide](audit-trail.md)
|
|
34
44
|
|
|
35
45
|
#### Protect production
|
|
36
46
|
See [Kill Switch Documentation](kill-switch.md)
|
|
37
47
|
|
|
38
|
-
#### Configure permissions
|
|
39
|
-
See [Configuration - Permission System](configuration.md#permission-system)
|
|
40
|
-
|
|
41
48
|
#### Use the console API
|
|
42
49
|
See [API Reference](api-reference.md)
|
|
43
50
|
|
|
44
51
|
#### Handle drift detection
|
|
45
52
|
See [Usage Guide - Drift Detection](usage-guide.md#drift-detection)
|
|
46
53
|
|
|
54
|
+
#### Troubleshoot issues
|
|
55
|
+
See [Troubleshooting Guide](troubleshooting.md)
|
|
56
|
+
|
|
47
57
|
## Screenshots
|
|
48
58
|
|
|
49
59
|
Screenshots referenced in the documentation are stored in the [screenshots](screenshots/) directory.
|