pg_sql_triggers 1.1.1 → 1.2.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/.rubocop.yml +15 -0
- data/CHANGELOG.md +56 -0
- data/COVERAGE.md +32 -28
- data/README.md +29 -0
- data/app/controllers/pg_sql_triggers/application_controller.rb +1 -1
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +10 -0
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +62 -10
- data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +161 -0
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +165 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +123 -0
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +40 -2
- data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +81 -0
- data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +85 -0
- data/app/views/pg_sql_triggers/tables/show.html.erb +36 -2
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +129 -0
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +136 -0
- data/app/views/pg_sql_triggers/triggers/show.html.erb +186 -0
- data/config/routes.rb +9 -0
- data/docs/api-reference.md +252 -4
- data/docs/web-ui.md +82 -17
- data/lib/pg_sql_triggers/registry/manager.rb +28 -13
- data/lib/pg_sql_triggers/registry.rb +41 -0
- data/lib/pg_sql_triggers/sql/capsule.rb +79 -0
- data/lib/pg_sql_triggers/sql/executor.rb +200 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- metadata +10 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PgSqlTriggers
|
|
4
|
+
# rubocop:disable Metrics/ClassLength
|
|
4
5
|
class TriggerRegistry < PgSqlTriggers::ApplicationRecord
|
|
5
6
|
self.table_name = "pg_sql_triggers_registry"
|
|
6
7
|
|
|
@@ -150,6 +151,51 @@ module PgSqlTriggers
|
|
|
150
151
|
end
|
|
151
152
|
end
|
|
152
153
|
|
|
154
|
+
def drop!(reason:, confirmation: nil, actor: nil)
|
|
155
|
+
# Check kill switch before dropping trigger
|
|
156
|
+
PgSqlTriggers::SQL::KillSwitch.check!(
|
|
157
|
+
operation: :trigger_drop,
|
|
158
|
+
environment: Rails.env,
|
|
159
|
+
confirmation: confirmation,
|
|
160
|
+
actor: actor || { type: "Console", id: "TriggerRegistry#drop!" }
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Validate reason is provided
|
|
164
|
+
raise ArgumentError, "Reason is required" if reason.nil? || reason.to_s.strip.empty?
|
|
165
|
+
|
|
166
|
+
log_drop_attempt(reason)
|
|
167
|
+
|
|
168
|
+
# Execute DROP TRIGGER in transaction
|
|
169
|
+
ActiveRecord::Base.transaction do
|
|
170
|
+
drop_trigger_from_database
|
|
171
|
+
destroy!
|
|
172
|
+
log_drop_success
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def re_execute!(reason:, confirmation: nil, actor: nil)
|
|
177
|
+
# Check kill switch before re-executing trigger
|
|
178
|
+
PgSqlTriggers::SQL::KillSwitch.check!(
|
|
179
|
+
operation: :trigger_re_execute,
|
|
180
|
+
environment: Rails.env,
|
|
181
|
+
confirmation: confirmation,
|
|
182
|
+
actor: actor || { type: "Console", id: "TriggerRegistry#re_execute!" }
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Validate reason is provided
|
|
186
|
+
raise ArgumentError, "Reason is required" if reason.nil? || reason.to_s.strip.empty?
|
|
187
|
+
raise StandardError, "Cannot re-execute: missing function_body" if function_body.blank?
|
|
188
|
+
|
|
189
|
+
log_re_execute_attempt(reason)
|
|
190
|
+
|
|
191
|
+
# Execute the trigger creation/update in transaction
|
|
192
|
+
ActiveRecord::Base.transaction do
|
|
193
|
+
drop_existing_trigger_for_re_execute
|
|
194
|
+
recreate_trigger
|
|
195
|
+
update_registry_after_re_execute
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
153
199
|
private
|
|
154
200
|
|
|
155
201
|
def quote_identifier(identifier)
|
|
@@ -170,5 +216,82 @@ module PgSqlTriggers
|
|
|
170
216
|
def verify!
|
|
171
217
|
update!(last_verified_at: Time.current)
|
|
172
218
|
end
|
|
219
|
+
|
|
220
|
+
# Drop trigger helpers
|
|
221
|
+
def log_drop_attempt(reason)
|
|
222
|
+
return unless defined?(Rails.logger)
|
|
223
|
+
|
|
224
|
+
Rails.logger.info "[TRIGGER_DROP] Dropping: #{trigger_name} on #{table_name}"
|
|
225
|
+
Rails.logger.info "[TRIGGER_DROP] Reason: #{reason}"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def log_drop_success
|
|
229
|
+
return unless defined?(Rails.logger)
|
|
230
|
+
|
|
231
|
+
Rails.logger.info "[TRIGGER_DROP] Successfully removed from registry"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def drop_trigger_from_database
|
|
235
|
+
trigger_exists = check_trigger_exists
|
|
236
|
+
return unless trigger_exists
|
|
237
|
+
|
|
238
|
+
execute_drop_sql
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def check_trigger_exists
|
|
242
|
+
introspection = PgSqlTriggers::DatabaseIntrospection.new
|
|
243
|
+
introspection.trigger_exists?(trigger_name)
|
|
244
|
+
rescue StandardError => e
|
|
245
|
+
Rails.logger.warn("Could not check trigger existence: #{e.message}") if defined?(Rails.logger)
|
|
246
|
+
false
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def execute_drop_sql
|
|
250
|
+
quoted_table = quote_identifier(table_name)
|
|
251
|
+
quoted_trigger = quote_identifier(trigger_name)
|
|
252
|
+
sql = "DROP TRIGGER IF EXISTS #{quoted_trigger} ON #{quoted_table};"
|
|
253
|
+
ActiveRecord::Base.connection.execute(sql)
|
|
254
|
+
Rails.logger.info "[TRIGGER_DROP] Dropped from database" if defined?(Rails.logger)
|
|
255
|
+
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
256
|
+
Rails.logger.error("[TRIGGER_DROP] Failed: #{e.message}") if defined?(Rails.logger)
|
|
257
|
+
raise
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Re-execute trigger helpers
|
|
261
|
+
def log_re_execute_attempt(reason)
|
|
262
|
+
return unless defined?(Rails.logger)
|
|
263
|
+
|
|
264
|
+
Rails.logger.info "[TRIGGER_RE_EXECUTE] Re-executing: #{trigger_name} on #{table_name}"
|
|
265
|
+
Rails.logger.info "[TRIGGER_RE_EXECUTE] Reason: #{reason}"
|
|
266
|
+
drift = drift_result
|
|
267
|
+
Rails.logger.info "[TRIGGER_RE_EXECUTE] Current state: #{drift[:state]}"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def drop_existing_trigger_for_re_execute
|
|
271
|
+
introspection = PgSqlTriggers::DatabaseIntrospection.new
|
|
272
|
+
return unless introspection.trigger_exists?(trigger_name)
|
|
273
|
+
|
|
274
|
+
quoted_table = quote_identifier(table_name)
|
|
275
|
+
quoted_trigger = quote_identifier(trigger_name)
|
|
276
|
+
drop_sql = "DROP TRIGGER IF EXISTS #{quoted_trigger} ON #{quoted_table};"
|
|
277
|
+
ActiveRecord::Base.connection.execute(drop_sql)
|
|
278
|
+
Rails.logger.info "[TRIGGER_RE_EXECUTE] Dropped existing" if defined?(Rails.logger)
|
|
279
|
+
rescue StandardError => e
|
|
280
|
+
Rails.logger.warn("[TRIGGER_RE_EXECUTE] Drop failed: #{e.message}") if defined?(Rails.logger)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def recreate_trigger
|
|
284
|
+
ActiveRecord::Base.connection.execute(function_body)
|
|
285
|
+
Rails.logger.info "[TRIGGER_RE_EXECUTE] Re-created trigger" if defined?(Rails.logger)
|
|
286
|
+
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
287
|
+
Rails.logger.error("[TRIGGER_RE_EXECUTE] Failed: #{e.message}") if defined?(Rails.logger)
|
|
288
|
+
raise
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def update_registry_after_re_execute
|
|
292
|
+
update!(enabled: true, last_executed_at: Time.current)
|
|
293
|
+
Rails.logger.info "[TRIGGER_RE_EXECUTE] Updated registry" if defined?(Rails.logger)
|
|
294
|
+
end
|
|
173
295
|
end
|
|
296
|
+
# rubocop:enable Metrics/ClassLength
|
|
174
297
|
end
|
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
<tbody>
|
|
47
47
|
<% @triggers.limit(10).each do |trigger| %>
|
|
48
48
|
<tr>
|
|
49
|
-
<td
|
|
49
|
+
<td>
|
|
50
|
+
<%= link_to trigger.trigger_name, trigger_path(trigger), style: "color: #007bff; text-decoration: none; font-weight: 600;" %>
|
|
51
|
+
</td>
|
|
50
52
|
<td><%= trigger.table_name %></td>
|
|
51
53
|
<td><%= trigger.version %></td>
|
|
52
54
|
<td>
|
|
@@ -57,7 +59,43 @@
|
|
|
57
59
|
<% end %>
|
|
58
60
|
</td>
|
|
59
61
|
<td><span class="badge badge-info"><%= trigger.source %></span></td>
|
|
60
|
-
<td
|
|
62
|
+
<td>
|
|
63
|
+
<% if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger) %>
|
|
64
|
+
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
65
|
+
<% if trigger.enabled %>
|
|
66
|
+
<% form_id = "trigger-disable-#{trigger.id}-form" %>
|
|
67
|
+
<%= form_with url: disable_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
|
|
68
|
+
<%= f.hidden_field :redirect_to, value: dashboard_path %>
|
|
69
|
+
<button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
|
|
70
|
+
style="padding: 0.25rem 0.75rem; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
71
|
+
Disable
|
|
72
|
+
</button>
|
|
73
|
+
<% end %>
|
|
74
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
75
|
+
operation: :ui_trigger_disable,
|
|
76
|
+
form_id: form_id,
|
|
77
|
+
title: 'Disable Trigger',
|
|
78
|
+
message: "Are you sure you want to disable trigger '#{trigger.trigger_name}'?" %>
|
|
79
|
+
<% else %>
|
|
80
|
+
<% form_id = "trigger-enable-#{trigger.id}-form" %>
|
|
81
|
+
<%= form_with url: enable_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
|
|
82
|
+
<%= f.hidden_field :redirect_to, value: dashboard_path %>
|
|
83
|
+
<button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
|
|
84
|
+
style="padding: 0.25rem 0.75rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
|
|
85
|
+
Enable
|
|
86
|
+
</button>
|
|
87
|
+
<% end %>
|
|
88
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
89
|
+
operation: :ui_trigger_enable,
|
|
90
|
+
form_id: form_id,
|
|
91
|
+
title: 'Enable Trigger',
|
|
92
|
+
message: "Are you sure you want to enable trigger '#{trigger.trigger_name}'?" %>
|
|
93
|
+
<% end %>
|
|
94
|
+
</div>
|
|
95
|
+
<% else %>
|
|
96
|
+
—
|
|
97
|
+
<% end %>
|
|
98
|
+
</td>
|
|
61
99
|
</tr>
|
|
62
100
|
<% end %>
|
|
63
101
|
</tbody>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<div style="max-width: 900px; margin: 0 auto;">
|
|
2
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
3
|
+
<h2 style="margin: 0;">Create SQL Capsule</h2>
|
|
4
|
+
<%= link_to "← Back to Dashboard", dashboard_path, style: "color: #007bff; text-decoration: none;" %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div style="background: #fff3cd; border: 1px solid #ffc107; padding: 1rem; border-radius: 4px; margin-bottom: 2rem;">
|
|
8
|
+
<strong>⚠️ Warning:</strong> SQL Capsules allow direct SQL execution. Use with extreme caution.
|
|
9
|
+
This feature is intended for emergency operations only.
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<%= form_with url: sql_capsules_path, method: :post, local: true do |f| %>
|
|
13
|
+
<div style="background: white; padding: 2rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
|
14
|
+
|
|
15
|
+
<div style="margin-bottom: 1.5rem;">
|
|
16
|
+
<label for="name" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
|
|
17
|
+
Capsule Name <span style="color: #dc3545;">*</span>
|
|
18
|
+
</label>
|
|
19
|
+
<%= text_field_tag :name, @capsule_name,
|
|
20
|
+
placeholder: "e.g., fix_user_permissions",
|
|
21
|
+
required: true,
|
|
22
|
+
pattern: "[a-zA-Z0-9_-]+",
|
|
23
|
+
autocomplete: "off",
|
|
24
|
+
style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" %>
|
|
25
|
+
<small style="color: #6c757d; display: block; margin-top: 0.25rem;">
|
|
26
|
+
Letters, numbers, underscores, and hyphens only
|
|
27
|
+
</small>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div style="margin-bottom: 1.5rem;">
|
|
31
|
+
<label for="environment" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
|
|
32
|
+
Environment <span style="color: #dc3545;">*</span>
|
|
33
|
+
</label>
|
|
34
|
+
<%= text_field_tag :environment, @environment,
|
|
35
|
+
placeholder: "e.g., production",
|
|
36
|
+
required: true,
|
|
37
|
+
autocomplete: "off",
|
|
38
|
+
style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" %>
|
|
39
|
+
<small style="color: #6c757d; display: block; margin-top: 0.25rem;">
|
|
40
|
+
The environment this capsule is intended for
|
|
41
|
+
</small>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div style="margin-bottom: 1.5rem;">
|
|
45
|
+
<label for="purpose" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
|
|
46
|
+
Purpose <span style="color: #dc3545;">*</span>
|
|
47
|
+
</label>
|
|
48
|
+
<%= text_area_tag :purpose, @purpose,
|
|
49
|
+
placeholder: "Describe what this SQL does and why it's needed...",
|
|
50
|
+
required: true,
|
|
51
|
+
rows: 3,
|
|
52
|
+
style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-family: inherit;" %>
|
|
53
|
+
<small style="color: #6c757d; display: block; margin-top: 0.25rem;">
|
|
54
|
+
Detailed description of the operation and its purpose
|
|
55
|
+
</small>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div style="margin-bottom: 1.5rem;">
|
|
59
|
+
<label for="sql" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
|
|
60
|
+
SQL Statement <span style="color: #dc3545;">*</span>
|
|
61
|
+
</label>
|
|
62
|
+
<%= text_area_tag :sql, @sql,
|
|
63
|
+
placeholder: "-- Enter your SQL statement here\nSELECT * FROM users WHERE ...",
|
|
64
|
+
required: true,
|
|
65
|
+
rows: 12,
|
|
66
|
+
style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-family: 'Monaco', 'Menlo', 'Courier New', monospace; font-size: 0.9rem;" %>
|
|
67
|
+
<small style="color: #6c757d; display: block; margin-top: 0.25rem;">
|
|
68
|
+
The SQL to execute. Review carefully before saving.
|
|
69
|
+
</small>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
|
|
73
|
+
<%= link_to "Cancel", dashboard_path,
|
|
74
|
+
style: "padding: 0.75rem 1.5rem; background: #6c757d; color: white; text-decoration: none; border-radius: 4px;" %>
|
|
75
|
+
<%= button_tag "Create Capsule",
|
|
76
|
+
type: "submit",
|
|
77
|
+
style: "padding: 0.75rem 1.5rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem;" %>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<% end %>
|
|
81
|
+
</div>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<div style="max-width: 900px; margin: 0 auto;">
|
|
2
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
3
|
+
<h2 style="margin: 0;">SQL Capsule: <%= @capsule.name %></h2>
|
|
4
|
+
<%= link_to "← Back to Dashboard", dashboard_path, style: "color: #007bff; text-decoration: none;" %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div style="background: white; padding: 2rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
|
|
8
|
+
<h3 style="margin-top: 0; margin-bottom: 1rem; color: #495057;">Capsule Details</h3>
|
|
9
|
+
|
|
10
|
+
<div style="display: grid; grid-template-columns: 200px 1fr; gap: 1rem; margin-bottom: 1rem;">
|
|
11
|
+
<div style="font-weight: 600; color: #6c757d;">Name:</div>
|
|
12
|
+
<div><%= @capsule.name %></div>
|
|
13
|
+
|
|
14
|
+
<div style="font-weight: 600; color: #6c757d;">Environment:</div>
|
|
15
|
+
<div><span class="badge badge-info"><%= @capsule.environment %></span></div>
|
|
16
|
+
|
|
17
|
+
<div style="font-weight: 600; color: #6c757d;">Created:</div>
|
|
18
|
+
<div><%= @capsule.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
|
|
19
|
+
|
|
20
|
+
<div style="font-weight: 600; color: #6c757d;">Checksum:</div>
|
|
21
|
+
<div style="font-family: 'Monaco', 'Menlo', 'Courier New', monospace; font-size: 0.85rem; color: #6c757d;">
|
|
22
|
+
<%= @checksum %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div style="margin-top: 1.5rem;">
|
|
27
|
+
<div style="font-weight: 600; color: #6c757d; margin-bottom: 0.5rem;">Purpose:</div>
|
|
28
|
+
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; border-left: 3px solid #007bff;">
|
|
29
|
+
<%= @capsule.purpose %>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div style="margin-top: 1.5rem;">
|
|
34
|
+
<div style="font-weight: 600; color: #6c757d; margin-bottom: 0.5rem;">SQL Statement:</div>
|
|
35
|
+
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; border-left: 3px solid #28a745; overflow-x: auto;">
|
|
36
|
+
<pre style="margin: 0; font-family: 'Monaco', 'Menlo', 'Courier New', monospace; font-size: 0.9rem; white-space: pre-wrap; word-wrap: break-word;"><%= @capsule.sql %></pre>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<% if @can_execute %>
|
|
42
|
+
<div style="background: #fff3cd; border: 1px solid #ffc107; padding: 1.5rem; border-radius: 4px; margin-bottom: 2rem;">
|
|
43
|
+
<h4 style="margin-top: 0; color: #856404;">⚠️ Execution Warning</h4>
|
|
44
|
+
<p style="margin-bottom: 1rem; color: #856404;">
|
|
45
|
+
This will execute the SQL statement directly against the database.
|
|
46
|
+
This operation cannot be undone. Make sure you understand the implications.
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<% if kill_switch_active? %>
|
|
50
|
+
<p style="margin-bottom: 1rem; color: #856404;">
|
|
51
|
+
<strong>Kill Switch is ACTIVE for <%= current_environment %> environment.</strong><br>
|
|
52
|
+
You must provide confirmation text to proceed.
|
|
53
|
+
</p>
|
|
54
|
+
<% end %>
|
|
55
|
+
|
|
56
|
+
<% form_id = "execute-capsule-form" %>
|
|
57
|
+
<%= form_with url: execute_sql_capsule_path(@capsule.name), method: :post, local: true, id: form_id do |f| %>
|
|
58
|
+
<div style="display: flex; gap: 1rem; align-items: flex-end;">
|
|
59
|
+
<div style="flex: 1;">
|
|
60
|
+
<button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
|
|
61
|
+
style="width: 100%; padding: 0.75rem 1.5rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: 600;">
|
|
62
|
+
Execute SQL Capsule
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
67
|
+
|
|
68
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
69
|
+
operation: :execute_sql_capsule,
|
|
70
|
+
form_id: form_id,
|
|
71
|
+
title: 'Execute SQL Capsule',
|
|
72
|
+
message: "Are you sure you want to execute SQL capsule '#{@capsule.name}'? This will run the SQL statement directly against the database." %>
|
|
73
|
+
</div>
|
|
74
|
+
<% else %>
|
|
75
|
+
<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; color: #721c24;">
|
|
76
|
+
<strong>Insufficient Permissions:</strong> You need Admin role to execute SQL capsules.
|
|
77
|
+
</div>
|
|
78
|
+
<% end %>
|
|
79
|
+
|
|
80
|
+
<div style="margin-top: 2rem;">
|
|
81
|
+
<%= link_to "Create Another Capsule", new_sql_capsule_path,
|
|
82
|
+
class: "btn btn-primary",
|
|
83
|
+
style: "padding: 0.75rem 1.5rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
<div style="border: 1px solid #dee2e6; border-radius: 4px; padding: 1rem; background: #f8f9fa;">
|
|
47
47
|
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
|
|
48
48
|
<div>
|
|
49
|
-
<h4 style="margin: 0;
|
|
50
|
-
<%= trigger.trigger_name %>
|
|
49
|
+
<h4 style="margin: 0;">
|
|
50
|
+
<%= link_to trigger.trigger_name, trigger_path(trigger), style: "color: #007bff; text-decoration: none;" %>
|
|
51
51
|
<% if trigger.enabled %>
|
|
52
52
|
<span class="badge badge-success">Enabled</span>
|
|
53
53
|
<% else %>
|
|
@@ -64,6 +64,40 @@
|
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
66
66
|
|
|
67
|
+
<% if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger) %>
|
|
68
|
+
<div style="margin-bottom: 1rem; display: flex; gap: 0.5rem;">
|
|
69
|
+
<% if trigger.enabled %>
|
|
70
|
+
<% form_id = "table-trigger-disable-#{trigger.id}-form" %>
|
|
71
|
+
<%= form_with url: disable_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
|
|
72
|
+
<%= f.hidden_field :redirect_to, value: table_path(@table_info[:table_name]) %>
|
|
73
|
+
<button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
|
|
74
|
+
style="padding: 0.5rem 1rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
|
|
75
|
+
Disable Trigger
|
|
76
|
+
</button>
|
|
77
|
+
<% end %>
|
|
78
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
79
|
+
operation: :ui_trigger_disable,
|
|
80
|
+
form_id: form_id,
|
|
81
|
+
title: 'Disable Trigger',
|
|
82
|
+
message: "Are you sure you want to disable trigger '#{trigger.trigger_name}' on table '#{@table_info[:table_name]}'?" %>
|
|
83
|
+
<% else %>
|
|
84
|
+
<% form_id = "table-trigger-enable-#{trigger.id}-form" %>
|
|
85
|
+
<%= form_with url: enable_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
|
|
86
|
+
<%= f.hidden_field :redirect_to, value: table_path(@table_info[:table_name]) %>
|
|
87
|
+
<button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
|
|
88
|
+
style="padding: 0.5rem 1rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
|
|
89
|
+
Enable Trigger
|
|
90
|
+
</button>
|
|
91
|
+
<% end %>
|
|
92
|
+
<%= render 'pg_sql_triggers/shared/confirmation_modal',
|
|
93
|
+
operation: :ui_trigger_enable,
|
|
94
|
+
form_id: form_id,
|
|
95
|
+
title: 'Enable Trigger',
|
|
96
|
+
message: "Are you sure you want to enable trigger '#{trigger.trigger_name}' on table '#{@table_info[:table_name]}'?" %>
|
|
97
|
+
<% end %>
|
|
98
|
+
</div>
|
|
99
|
+
<% end %>
|
|
100
|
+
|
|
67
101
|
<% if trigger.definition.present? %>
|
|
68
102
|
<% definition = JSON.parse(trigger.definition) rescue {} %>
|
|
69
103
|
<div style="margin-bottom: 1rem;">
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<% form_id = "trigger-drop-#{trigger.id}-form" %>
|
|
2
|
+
|
|
3
|
+
<%= 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: params[:redirect_to] || trigger_path(trigger) %>
|
|
5
|
+
|
|
6
|
+
<button type="button" onclick="showDropModal('<%= form_id %>')"
|
|
7
|
+
style="padding: 0.75rem 1.5rem; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
|
|
8
|
+
Drop Trigger
|
|
9
|
+
</button>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<!-- Drop Modal -->
|
|
13
|
+
<div id="<%= form_id %>-modal" style="display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4);">
|
|
14
|
+
<div style="background-color: #fefefe; margin: 5% auto; padding: 0; border-radius: 8px; width: 90%; max-width: 600px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
|
15
|
+
<!-- Header -->
|
|
16
|
+
<div style="background: #dc3545; color: white; padding: 1.5rem; border-radius: 8px 8px 0 0;">
|
|
17
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
18
|
+
<h3 style="margin: 0;">⚠️ Drop Trigger</h3>
|
|
19
|
+
<button type="button" onclick="closeDropModal('<%= form_id %>')"
|
|
20
|
+
style="background: none; border: none; color: white; font-size: 1.5rem; cursor: pointer; padding: 0; width: 30px; height: 30px;">
|
|
21
|
+
×
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- Body -->
|
|
27
|
+
<div style="padding: 1.5rem;">
|
|
28
|
+
<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 1rem; margin-bottom: 1.5rem;">
|
|
29
|
+
<strong style="color: #856404;">Warning:</strong>
|
|
30
|
+
<p style="margin: 0.5rem 0 0 0; color: #856404;">
|
|
31
|
+
This will permanently drop trigger '<strong><%= trigger.trigger_name %></strong>' from the database and remove it from the registry.
|
|
32
|
+
This action cannot be undone.
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<form id="<%= form_id %>-reason-form">
|
|
37
|
+
<div style="margin-bottom: 1rem;">
|
|
38
|
+
<label for="<%= form_id %>-reason" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
|
|
39
|
+
Reason <span style="color: #dc3545;">*</span>
|
|
40
|
+
</label>
|
|
41
|
+
<textarea id="<%= form_id %>-reason" name="reason" required rows="3"
|
|
42
|
+
placeholder="Why are you dropping this trigger? This will be logged."
|
|
43
|
+
style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-family: inherit;"></textarea>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<% if kill_switch_active? %>
|
|
47
|
+
<div style="margin-bottom: 1rem;">
|
|
48
|
+
<label for="<%= form_id %>-confirmation" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
|
|
49
|
+
Confirmation Text <span style="color: #dc3545;">*</span>
|
|
50
|
+
</label>
|
|
51
|
+
<input type="text" id="<%= form_id %>-confirmation" name="confirmation_text" required
|
|
52
|
+
placeholder="<%= expected_confirmation_text(:trigger_drop) %>"
|
|
53
|
+
autocomplete="off"
|
|
54
|
+
style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-family: 'Monaco', 'Menlo', monospace;">
|
|
55
|
+
<small style="color: #6c757d; display: block; margin-top: 0.25rem;">
|
|
56
|
+
Type: <code><%= expected_confirmation_text(:trigger_drop) %></code>
|
|
57
|
+
</small>
|
|
58
|
+
</div>
|
|
59
|
+
<% end %>
|
|
60
|
+
|
|
61
|
+
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
|
|
62
|
+
<button type="button" onclick="closeDropModal('<%= form_id %>')"
|
|
63
|
+
style="padding: 0.75rem 1.5rem; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
|
64
|
+
Cancel
|
|
65
|
+
</button>
|
|
66
|
+
<button type="button" onclick="submitDrop('<%= form_id %>')"
|
|
67
|
+
style="padding: 0.75rem 1.5rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">
|
|
68
|
+
Drop Trigger
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</form>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<script>
|
|
77
|
+
function showDropModal(formId) {
|
|
78
|
+
document.getElementById(formId + '-modal').style.display = 'block';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function closeDropModal(formId) {
|
|
82
|
+
document.getElementById(formId + '-modal').style.display = 'none';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function submitDrop(formId) {
|
|
86
|
+
const form = document.getElementById(formId);
|
|
87
|
+
const reasonForm = document.getElementById(formId + '-reason-form');
|
|
88
|
+
const reason = document.getElementById(formId + '-reason').value;
|
|
89
|
+
|
|
90
|
+
if (!reason.trim()) {
|
|
91
|
+
alert('Please provide a reason for dropping this trigger.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
<% if kill_switch_active? %>
|
|
96
|
+
const confirmation = document.getElementById(formId + '-confirmation').value;
|
|
97
|
+
if (!confirmation.trim()) {
|
|
98
|
+
alert('Please provide the confirmation text.');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add confirmation to main form
|
|
103
|
+
const confirmationInput = document.createElement('input');
|
|
104
|
+
confirmationInput.type = 'hidden';
|
|
105
|
+
confirmationInput.name = 'confirmation_text';
|
|
106
|
+
confirmationInput.value = confirmation;
|
|
107
|
+
form.appendChild(confirmationInput);
|
|
108
|
+
<% end %>
|
|
109
|
+
|
|
110
|
+
// Add reason to main form
|
|
111
|
+
const reasonInput = document.createElement('input');
|
|
112
|
+
reasonInput.type = 'hidden';
|
|
113
|
+
reasonInput.name = 'reason';
|
|
114
|
+
reasonInput.value = reason;
|
|
115
|
+
form.appendChild(reasonInput);
|
|
116
|
+
|
|
117
|
+
form.submit();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Close modal when clicking outside
|
|
121
|
+
window.onclick = function(event) {
|
|
122
|
+
const modals = document.querySelectorAll('[id$="-modal"]');
|
|
123
|
+
modals.forEach(modal => {
|
|
124
|
+
if (event.target == modal) {
|
|
125
|
+
modal.style.display = 'none';
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
</script>
|