rails_audit_log 0.7.0 → 0.9.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -0
  3. data/app/assets/stylesheets/rails_audit_log/_01_base.css +32 -0
  4. data/app/assets/stylesheets/rails_audit_log/_02_layout.css +34 -0
  5. data/app/assets/stylesheets/rails_audit_log/_03_table.css +39 -0
  6. data/app/assets/stylesheets/rails_audit_log/_04_badges.css +13 -0
  7. data/app/assets/stylesheets/rails_audit_log/_05_diff.css +94 -0
  8. data/app/assets/stylesheets/rails_audit_log/_06_timeline.css +21 -0
  9. data/app/assets/stylesheets/rails_audit_log/_07_detail.css +15 -0
  10. data/app/assets/stylesheets/rails_audit_log/_08_pagination.css +56 -0
  11. data/app/assets/stylesheets/rails_audit_log/_09_filters.css +54 -0
  12. data/app/assets/stylesheets/rails_audit_log/application.css +1 -15
  13. data/app/concerns/rails_audit_log/auditable.rb +14 -0
  14. data/app/controllers/rails_audit_log/application_controller.rb +12 -0
  15. data/app/controllers/rails_audit_log/audit_log_entries_controller.rb +31 -0
  16. data/app/controllers/rails_audit_log/resources_controller.rb +13 -0
  17. data/app/helpers/rails_audit_log/application_helper.rb +13 -0
  18. data/app/javascript/rails_audit_log/application.js +8 -0
  19. data/app/javascript/rails_audit_log/diff_controller.js +20 -0
  20. data/app/javascript/rails_audit_log/search_controller.js +12 -0
  21. data/app/models/rails_audit_log/audit_log_entry.rb +5 -3
  22. data/app/views/layouts/rails_audit_log/application.html.erb +16 -10
  23. data/app/views/rails_audit_log/audit_log_entries/_diff.html.erb +55 -0
  24. data/app/views/rails_audit_log/audit_log_entries/index.html.erb +76 -0
  25. data/app/views/rails_audit_log/audit_log_entries/show.html.erb +50 -0
  26. data/app/views/rails_audit_log/resources/show.html.erb +35 -0
  27. data/config/importmap.rb +5 -0
  28. data/config/routes.rb +3 -0
  29. data/lib/generators/rails_audit_log/initializer/initializer_generator.rb +15 -0
  30. data/lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb +41 -0
  31. data/lib/rails_audit_log/engine.rb +28 -0
  32. data/lib/rails_audit_log/matchers.rb +103 -0
  33. data/lib/rails_audit_log/minitest_assertions.rb +31 -0
  34. data/lib/rails_audit_log/test_helpers.rb +7 -0
  35. data/lib/rails_audit_log/version.rb +1 -1
  36. data/lib/rails_audit_log.rb +10 -0
  37. metadata +67 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3323e92d5f10ecd832a198a65bf6e0fd65c7bf0ff35069230bc8b7791d14e7fa
4
- data.tar.gz: 4cd882a647d273ee3fe8a6c7b7f89a3b241ee0fa9b22b84aed93b8aea2a30c6d
3
+ metadata.gz: 9c9a9fcdb866f5300a3a2fd9bd6026ad93530d15b0b26d771ef1a28a47985280
4
+ data.tar.gz: 01ac4b3f95fd1c2067a8aa5936122ff86dc264fe771881515dd31d3401377b66
5
5
  SHA512:
6
- metadata.gz: 7c8d577a7b027d8c300b62545930e94798967f5cdc7f57d4d84a06b3f1968d1357b6ea1db9ae8abf1cb3e36bda8e6fd12be6c2da0798cf7d6d6a02e60849621c
7
- data.tar.gz: 69a905ee623f28403c53d280a56009cd81696406e22ff2f258ef2d0ec56fc4d7a154f72a0a345557ec8a39e0b36b655b3fba7c1a128bee954834e73d537946b6
6
+ metadata.gz: 4c155747f1219996a6fde0fcc0d00c764df0f296c7b099418b7020ef90984690400300174b5c1cc1dc4842451a3f53d35e4b710542693683c78d4706c0eca1c1
7
+ data.tar.gz: 256ef73b739193443718481ef7fa1c0920ae4b4d424e3466b9ac3dbaf9a0bf8b7cf4823d0fd7d000652710985ac60fa6df6252e5997266bb40eda6bde9ba9941
data/README.md CHANGED
@@ -23,6 +23,24 @@ bin/rails generate rails_audit_log:install
23
23
  bin/rails db:migrate
24
24
  ```
25
25
 
26
+ Optionally scaffold a commented initializer with every configuration option:
27
+
28
+ ```bash
29
+ bin/rails generate rails_audit_log:initializer
30
+ ```
31
+
32
+ This creates `config/initializers/rails_audit_log.rb` with all settings documented as commented examples inside a `RailsAuditLog.configure` block.
33
+
34
+ ## Web dashboard
35
+
36
+ Mount the engine in `config/routes.rb` to enable the built-in audit trail browser:
37
+
38
+ ```ruby
39
+ mount RailsAuditLog::Engine, at: "/audit"
40
+ ```
41
+
42
+ Then visit `/audit` to browse all audit entries. The dashboard is delivered via propshaft, importmaps, and CDN-pinned Turbo and Stimulus — no asset pipeline configuration required in the host app.
43
+
26
44
  ## Usage
27
45
 
28
46
  ### Tracking a model
@@ -384,6 +402,76 @@ To save storage at the cost of reduced reification accuracy, switch to diff-only
384
402
  RailsAuditLog.store_snapshot = false
385
403
  ```
386
404
 
405
+ ### Test helper
406
+
407
+ `without_audit_log` silences audit tracking inside the block — useful in FactoryBot factories and seed data to avoid noise in the audit trail:
408
+
409
+ ```ruby
410
+ # spec/rails_helper.rb (or spec/support/factory_helpers.rb)
411
+ require "rails_audit_log/test_helpers"
412
+
413
+ RSpec.configure do |config|
414
+ config.include RailsAuditLog::TestHelpers
415
+ end
416
+
417
+ # Or include directly in FactoryBot definitions:
418
+ FactoryBot.define do
419
+ factory :post do
420
+ after(:create) { |p| without_audit_log { p.update!(cached_at: Time.current) } }
421
+ end
422
+ end
423
+ ```
424
+
425
+ `without_audit_log` is a prefix-free wrapper around `RailsAuditLog.disable` — thread-safe and restores tracking even if the block raises.
426
+
427
+ ### Minitest assertions
428
+
429
+ Add to your `test/test_helper.rb`:
430
+
431
+ ```ruby
432
+ require "rails_audit_log/minitest_assertions"
433
+
434
+ class ActiveSupport::TestCase
435
+ include RailsAuditLog::MinitestAssertions
436
+ end
437
+ ```
438
+
439
+ Then use the assertions in any test:
440
+
441
+ ```ruby
442
+ assert_audit_log_entry post # any entry
443
+ assert_audit_log_entry post, event: :update # update entry
444
+ assert_audit_log_entry post, event: :update, touching: :title # touching title
445
+ refute_audit_log_entry post, event: :update # no update entry
446
+ assert_audit_log_entry post, event: :update, message: "custom" # custom failure message
447
+ ```
448
+
449
+ ### RSpec matchers
450
+
451
+ Add to your `spec/rails_helper.rb` (or `spec_helper.rb`):
452
+
453
+ ```ruby
454
+ require "rails_audit_log/matchers"
455
+
456
+ RSpec.configure do |config|
457
+ config.include RailsAuditLog::Matchers
458
+ end
459
+ ```
460
+
461
+ Then use the matchers in any spec:
462
+
463
+ ```ruby
464
+ # Assert a record has a matching audit entry
465
+ expect(post).to have_audit_log_entry
466
+ expect(post).to have_audit_log_entry(:update)
467
+ expect(post).to have_audit_log_entry(:update).touching(:title)
468
+
469
+ # Assert a block creates a matching audit entry
470
+ expect { post.update!(title: "New") }.to create_audit_log_entry
471
+ expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update)
472
+ expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update).touching(:title)
473
+ ```
474
+
387
475
  ## Requirements
388
476
 
389
477
  - Ruby >= 3.3
@@ -0,0 +1,32 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ :root {
4
+ --ral-bg: #f5f5f5;
5
+ --ral-surface: #ffffff;
6
+ --ral-border: #e5e5e5;
7
+ --ral-border-subtle: #f0f0f0;
8
+ --ral-text: #1a1a1a;
9
+ --ral-muted: #666666;
10
+ --ral-primary: #1d4ed8;
11
+ --ral-danger: #991b1b;
12
+ --ral-success: #166534;
13
+ --ral-radius: 6px;
14
+ --ral-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
15
+ --ral-surface-hover: #fafafa;
16
+ }
17
+
18
+ body {
19
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
20
+ font-size: 14px;
21
+ line-height: 1.5;
22
+ color: var(--ral-text);
23
+ background: var(--ral-bg);
24
+ min-height: 100vh;
25
+ }
26
+
27
+ .ral-link { color: var(--ral-primary); text-decoration: none; }
28
+ .ral-link:hover { text-decoration: underline; }
29
+
30
+ .ral-muted { color: var(--ral-muted); font-size: 13px; }
31
+ .ral-timestamp { color: var(--ral-muted); font-size: 13px; white-space: nowrap; }
32
+ .ral-reason { font-size: 12px; color: var(--ral-muted); font-style: italic; }
@@ -0,0 +1,34 @@
1
+ .ral-header {
2
+ background: var(--ral-text);
3
+ color: #fff;
4
+ height: 48px;
5
+ display: flex;
6
+ align-items: center;
7
+ padding: 0 24px;
8
+ }
9
+
10
+ .ral-header__inner {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 16px;
14
+ width: 100%;
15
+ max-width: 1200px;
16
+ margin: 0 auto;
17
+ }
18
+
19
+ .ral-header__logo {
20
+ color: #fff;
21
+ text-decoration: none;
22
+ font-weight: 600;
23
+ font-size: 15px;
24
+ }
25
+ .ral-header__logo:hover { opacity: 0.8; }
26
+
27
+ .ral-main { max-width: 1200px; margin: 0 auto; padding: 24px; }
28
+
29
+ .ral-page-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 20px; }
30
+ .ral-page-header__title { font-size: 20px; font-weight: 600; }
31
+ .ral-page-header__meta { font-size: 13px; color: var(--ral-muted); }
32
+
33
+ .ral-breadcrumb { font-size: 12px; color: var(--ral-muted); margin-bottom: 4px; }
34
+ .ral-section-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; color: #333; }
@@ -0,0 +1,39 @@
1
+ .ral-table-wrap {
2
+ background: var(--ral-surface);
3
+ border: 1px solid var(--ral-border);
4
+ border-radius: var(--ral-radius);
5
+ overflow: hidden;
6
+ box-shadow: var(--ral-shadow);
7
+ }
8
+
9
+ .ral-table { width: 100%; border-collapse: collapse; }
10
+
11
+ .ral-table th {
12
+ background: var(--ral-bg);
13
+ padding: 10px 16px;
14
+ text-align: left;
15
+ font-size: 12px;
16
+ font-weight: 600;
17
+ text-transform: uppercase;
18
+ letter-spacing: 0.04em;
19
+ color: var(--ral-muted);
20
+ border-bottom: 1px solid var(--ral-border);
21
+ }
22
+
23
+ .ral-table td {
24
+ padding: 10px 16px;
25
+ border-bottom: 1px solid var(--ral-border-subtle);
26
+ vertical-align: middle;
27
+ }
28
+
29
+ .ral-table tbody tr:last-child td { border-bottom: none; }
30
+ .ral-table tbody tr:hover { background: var(--ral-surface-hover); }
31
+
32
+ .ral-empty {
33
+ background: var(--ral-surface);
34
+ border: 1px solid var(--ral-border);
35
+ border-radius: var(--ral-radius);
36
+ padding: 40px;
37
+ text-align: center;
38
+ color: var(--ral-muted);
39
+ }
@@ -0,0 +1,13 @@
1
+ .ral-badge {
2
+ display: inline-block;
3
+ padding: 2px 8px;
4
+ border-radius: 10px;
5
+ font-size: 11px;
6
+ font-weight: 600;
7
+ text-transform: uppercase;
8
+ letter-spacing: 0.04em;
9
+ }
10
+
11
+ .ral-badge--create { background: #dcfce7; color: var(--ral-success); }
12
+ .ral-badge--update { background: #dbeafe; color: var(--ral-primary); }
13
+ .ral-badge--destroy { background: #fee2e2; color: var(--ral-danger); }
@@ -0,0 +1,94 @@
1
+ /* Toggle */
2
+ .ral-diff-toggle {
3
+ display: flex;
4
+ border: 1px solid var(--ral-border);
5
+ border-radius: var(--ral-radius);
6
+ overflow: hidden;
7
+ width: fit-content;
8
+ margin-bottom: 12px;
9
+ }
10
+
11
+ .ral-diff-btn {
12
+ padding: 4px 12px;
13
+ font-size: 12px;
14
+ font-weight: 500;
15
+ color: var(--ral-muted);
16
+ background: var(--ral-surface);
17
+ border: none;
18
+ cursor: pointer;
19
+ }
20
+ .ral-diff-btn + .ral-diff-btn { border-left: 1px solid var(--ral-border); }
21
+ .ral-diff-btn:hover:not(.ral-diff-btn--active) { background: var(--ral-bg); color: var(--ral-text); }
22
+ .ral-diff-btn--active { background: var(--ral-primary); color: #fff; }
23
+
24
+ /* Inline table */
25
+ .ral-diff { width: 100%; border-collapse: collapse; font-size: 13px; }
26
+
27
+ .ral-diff th {
28
+ background: var(--ral-bg);
29
+ padding: 6px 12px;
30
+ text-align: left;
31
+ font-size: 11px;
32
+ font-weight: 600;
33
+ text-transform: uppercase;
34
+ letter-spacing: 0.04em;
35
+ color: var(--ral-muted);
36
+ border-bottom: 1px solid var(--ral-border);
37
+ }
38
+
39
+ .ral-diff td { padding: 6px 12px; border-bottom: 1px solid var(--ral-border-subtle); vertical-align: top; }
40
+ .ral-diff tbody tr:last-child td { border-bottom: none; }
41
+
42
+ .ral-diff__attr { font-weight: 500; color: #333; width: 160px; }
43
+ .ral-diff__before { color: var(--ral-danger); background: #fff5f5; font-family: monospace; }
44
+ .ral-diff__after { color: var(--ral-success); background: #f0fdf4; font-family: monospace; }
45
+
46
+ /* Side-by-side panels */
47
+ .ral-diff-side {
48
+ display: grid;
49
+ grid-template-columns: 1fr 1fr;
50
+ border: 1px solid var(--ral-border);
51
+ border-radius: var(--ral-radius);
52
+ overflow: hidden;
53
+ font-size: 13px;
54
+ }
55
+
56
+ .ral-diff-side__panel--before { border-right: 1px solid var(--ral-border); }
57
+
58
+ .ral-diff-side__header {
59
+ padding: 6px 12px;
60
+ font-size: 11px;
61
+ font-weight: 600;
62
+ text-transform: uppercase;
63
+ letter-spacing: 0.04em;
64
+ color: var(--ral-muted);
65
+ border-bottom: 1px solid var(--ral-border);
66
+ background: var(--ral-bg);
67
+ }
68
+
69
+ .ral-diff-side__row {
70
+ display: flex;
71
+ gap: 8px;
72
+ padding: 6px 12px;
73
+ border-bottom: 1px solid var(--ral-border-subtle);
74
+ align-items: baseline;
75
+ }
76
+ .ral-diff-side__row:last-child { border-bottom: none; }
77
+
78
+ .ral-diff-side__attr { font-weight: 500; color: #333; min-width: 100px; flex-shrink: 0; }
79
+
80
+ .ral-diff-side__panel--before .ral-diff-side__value {
81
+ color: var(--ral-danger);
82
+ font-family: monospace;
83
+ background: #fff5f5;
84
+ padding: 1px 4px;
85
+ border-radius: 3px;
86
+ }
87
+
88
+ .ral-diff-side__panel--after .ral-diff-side__value {
89
+ color: var(--ral-success);
90
+ font-family: monospace;
91
+ background: #f0fdf4;
92
+ padding: 1px 4px;
93
+ border-radius: 3px;
94
+ }
@@ -0,0 +1,21 @@
1
+ .ral-timeline { display: flex; flex-direction: column; gap: 16px; }
2
+
3
+ .ral-timeline__entry {
4
+ background: var(--ral-surface);
5
+ border: 1px solid var(--ral-border);
6
+ border-radius: var(--ral-radius);
7
+ overflow: hidden;
8
+ box-shadow: var(--ral-shadow);
9
+ }
10
+
11
+ .ral-timeline__header {
12
+ display: flex;
13
+ align-items: center;
14
+ gap: 12px;
15
+ padding: 10px 16px;
16
+ background: var(--ral-bg);
17
+ border-bottom: 1px solid var(--ral-border);
18
+ font-size: 13px;
19
+ }
20
+
21
+ .ral-timeline__detail-link { margin-left: auto; }
@@ -0,0 +1,15 @@
1
+ .ral-card {
2
+ background: var(--ral-surface);
3
+ border: 1px solid var(--ral-border);
4
+ border-radius: var(--ral-radius);
5
+ padding: 16px;
6
+ margin-bottom: 20px;
7
+ box-shadow: var(--ral-shadow);
8
+ }
9
+
10
+ .ral-meta { display: grid; gap: 8px; }
11
+ .ral-meta__row { display: flex; gap: 16px; font-size: 13px; }
12
+ .ral-meta__row dt { width: 80px; color: var(--ral-muted); flex-shrink: 0; }
13
+ .ral-meta__row dd { color: var(--ral-text); }
14
+
15
+ .ral-entry-nav { display: flex; gap: 8px; align-items: center; }
@@ -0,0 +1,56 @@
1
+ nav.pagy.series-nav {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 4px;
5
+ margin-top: 16px;
6
+ font-size: 13px;
7
+ }
8
+
9
+ nav.pagy.series-nav a {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ min-width: 32px;
14
+ height: 32px;
15
+ padding: 0 8px;
16
+ border: 1px solid var(--ral-border);
17
+ border-radius: 4px;
18
+ background: var(--ral-surface);
19
+ color: var(--ral-primary);
20
+ text-decoration: none;
21
+ }
22
+
23
+ nav.pagy.series-nav a:hover:not([aria-disabled="true"]) { background: var(--ral-bg); }
24
+
25
+ nav.pagy.series-nav a[aria-current="page"] {
26
+ background: var(--ral-primary);
27
+ color: #fff;
28
+ border-color: var(--ral-primary);
29
+ font-weight: 600;
30
+ }
31
+
32
+ nav.pagy.series-nav a[aria-disabled="true"] {
33
+ color: #aaa;
34
+ cursor: default;
35
+ border-color: var(--ral-border-subtle);
36
+ }
37
+
38
+ nav.pagy.series-nav a[role="separator"] {
39
+ border: none;
40
+ color: var(--ral-muted);
41
+ min-width: auto;
42
+ }
43
+
44
+ .ral-pagination__link {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ height: 32px;
48
+ padding: 0 10px;
49
+ border: 1px solid var(--ral-border);
50
+ border-radius: 4px;
51
+ background: var(--ral-surface);
52
+ color: var(--ral-primary);
53
+ text-decoration: none;
54
+ font-size: 13px;
55
+ }
56
+ .ral-pagination__link:hover { background: var(--ral-bg); text-decoration: none; }
@@ -0,0 +1,54 @@
1
+ .ral-filters {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 8px;
5
+ flex-wrap: wrap;
6
+ margin-bottom: 16px;
7
+ }
8
+
9
+ .ral-filter-input {
10
+ padding: 5px 10px;
11
+ font-size: 13px;
12
+ border: 1px solid var(--ral-border);
13
+ border-radius: var(--ral-radius);
14
+ background: var(--ral-surface);
15
+ color: var(--ral-text);
16
+ min-width: 180px;
17
+ }
18
+ .ral-filter-input:focus { outline: 2px solid var(--ral-primary); outline-offset: -1px; }
19
+
20
+ .ral-filter-select {
21
+ padding: 5px 8px;
22
+ font-size: 13px;
23
+ border: 1px solid var(--ral-border);
24
+ border-radius: var(--ral-radius);
25
+ background: var(--ral-surface);
26
+ color: var(--ral-text);
27
+ cursor: pointer;
28
+ }
29
+
30
+ .ral-period-filter {
31
+ display: flex;
32
+ border: 1px solid var(--ral-border);
33
+ border-radius: var(--ral-radius);
34
+ overflow: hidden;
35
+ margin-left: auto;
36
+ }
37
+
38
+ .ral-period-btn {
39
+ padding: 5px 10px;
40
+ font-size: 13px;
41
+ font-weight: 500;
42
+ color: var(--ral-muted);
43
+ background: var(--ral-surface);
44
+ text-decoration: none;
45
+ }
46
+ .ral-period-btn + .ral-period-btn { border-left: 1px solid var(--ral-border); }
47
+ .ral-period-btn:hover:not(.ral-period-btn--active) {
48
+ background: var(--ral-bg);
49
+ color: var(--ral-text);
50
+ text-decoration: none;
51
+ }
52
+ .ral-period-btn--active { background: var(--ral-primary); color: #fff; }
53
+
54
+ .ral-filters__clear { font-size: 13px; }
@@ -1,15 +1 @@
1
- /*
2
- * This is a manifest file that'll be compiled into application.css, which will include all the files
3
- * listed below.
4
- *
5
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
- * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
- *
8
- * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
- * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
- * files in this directory. Styles in this file should be added after the last require_* statement.
11
- * It is generally better to create a new file per style scope.
12
- *
13
- *= require_tree .
14
- *= require_self
15
- */
1
+ /* Styles are loaded via the dashboard_stylesheets helper which globs _*.css files. */
@@ -10,6 +10,8 @@ module RailsAuditLog
10
10
  class_attribute :_audit_log_version_limit, default: nil
11
11
  class_attribute :_audit_log_async, default: false
12
12
 
13
+ _warn_if_audit_table_missing
14
+
13
15
  has_many :audit_log_entries,
14
16
  class_name: "RailsAuditLog::AuditLogEntry",
15
17
  as: :item,
@@ -52,6 +54,18 @@ module RailsAuditLog
52
54
  end
53
55
 
54
56
  class_methods do
57
+ def _warn_if_audit_table_missing
58
+ return if connection.table_exists?("audit_log_entries")
59
+
60
+ warn "[RailsAuditLog] WARNING: #{name} includes RailsAuditLog::Auditable but the " \
61
+ "'audit_log_entries' table does not exist. " \
62
+ "Run `bin/rails generate rails_audit_log:install && bin/rails db:migrate` to create it."
63
+ rescue ActiveRecord::NoDatabaseError,
64
+ ActiveRecord::ConnectionNotEstablished,
65
+ ActiveRecord::StatementInvalid
66
+ # DB not reachable during this phase (e.g. before db:create) — skip the check
67
+ end
68
+
55
69
  def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, async: nil)
56
70
  self._audit_log_only = only.map(&:to_s) if only
57
71
  self._audit_log_ignore = ignore.map(&:to_s) if ignore
@@ -1,4 +1,16 @@
1
1
  module RailsAuditLog
2
2
  class ApplicationController < ActionController::Base
3
+ include Pagy::Method
4
+
5
+ protect_from_forgery with: :exception
6
+ before_action :authenticate!
7
+
8
+ private
9
+
10
+ def authenticate!
11
+ return unless (auth = RailsAuditLog.authenticate)
12
+
13
+ instance_exec(self, &auth) || request_http_basic_authentication("Audit Log")
14
+ end
3
15
  end
4
16
  end
@@ -0,0 +1,31 @@
1
+ module RailsAuditLog
2
+ class AuditLogEntriesController < ApplicationController
3
+ def index
4
+ set_filters
5
+ @pagy, @entries = pagy(filtered_scope)
6
+ @item_types = AuditLogEntry.distinct.order(:item_type).pluck(:item_type)
7
+ end
8
+
9
+ def show
10
+ @entry = AuditLogEntry.find(params[:id])
11
+ end
12
+
13
+ private
14
+
15
+ def set_filters
16
+ @event = params[:event].presence_in(AuditLogEntry::EVENTS)
17
+ @item_type = params[:item_type].presence
18
+ @period = params[:period].presence_in(AuditLogEntry::PERIODS.keys)
19
+ @q = params[:q].presence
20
+ end
21
+
22
+ def filtered_scope
23
+ scope = AuditLogEntry.order(created_at: :desc)
24
+ scope = scope.where(event: @event) if @event
25
+ scope = scope.where(item_type: @item_type) if @item_type
26
+ scope = scope.for_period(@period) if @period
27
+ scope = scope.where("whodunnit_snapshot LIKE ?", "%#{@q}%") if @q
28
+ scope
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ module RailsAuditLog
2
+ class ResourcesController < ApplicationController
3
+ def show
4
+ @item_type = params[:item_type]
5
+ @item_id = params[:item_id]
6
+ @pagy, @entries = pagy(
7
+ AuditLogEntry
8
+ .where(item_type: @item_type, item_id: @item_id)
9
+ .order(created_at: :asc)
10
+ )
11
+ end
12
+ end
13
+ end
@@ -1,4 +1,17 @@
1
1
  module RailsAuditLog
2
2
  module ApplicationHelper
3
+ def format_diff_value(value)
4
+ return "—" if value.nil?
5
+ return "#{value["type"]} ##{value["id"]}" if value.is_a?(Hash)
6
+
7
+ value.inspect
8
+ end
9
+
10
+ def dashboard_stylesheets
11
+ dir = RailsAuditLog::Engine.root.join("app/assets/stylesheets/rails_audit_log")
12
+ dir.glob("_*.css").sort.map do |file|
13
+ stylesheet_link_tag("rails_audit_log/#{file.basename}", media: "all")
14
+ end.join("\n").html_safe
15
+ end
3
16
  end
4
17
  end
@@ -0,0 +1,8 @@
1
+ import "@hotwired/turbo"
2
+ import { Application } from "@hotwired/stimulus"
3
+ import SearchController from "rails_audit_log/search_controller"
4
+ import DiffController from "rails_audit_log/diff_controller"
5
+
6
+ const application = Application.start()
7
+ application.register("search", SearchController)
8
+ application.register("diff", DiffController)
@@ -0,0 +1,20 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class DiffController extends Controller {
4
+ static targets = ["inline", "side", "inlineBtn", "sideBtn"]
5
+
6
+ connect() {
7
+ this.setMode(localStorage.getItem("ral-diff-mode") || "inline")
8
+ }
9
+
10
+ setInline() { this.setMode("inline") }
11
+ setSide() { this.setMode("side") }
12
+
13
+ setMode(mode) {
14
+ localStorage.setItem("ral-diff-mode", mode)
15
+ this.inlineTarget.hidden = mode !== "inline"
16
+ this.sideTarget.hidden = mode !== "side"
17
+ this.inlineBtnTarget.classList.toggle("ral-diff-btn--active", mode === "inline")
18
+ this.sideBtnTarget.classList.toggle("ral-diff-btn--active", mode === "side")
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class SearchController extends Controller {
4
+ filter() {
5
+ clearTimeout(this._timeout)
6
+ this._timeout = setTimeout(() => this.element.requestSubmit(), 300)
7
+ }
8
+
9
+ select() {
10
+ this.element.requestSubmit()
11
+ }
12
+ }
@@ -2,8 +2,9 @@ module RailsAuditLog
2
2
  class AuditLogEntry < ApplicationRecord
3
3
  self.table_name = "audit_log_entries"
4
4
 
5
- EVENTS = %w[create update destroy].freeze
5
+ EVENTS = %w[create update destroy].freeze
6
6
  BLOB_COLUMNS = %w[object_changes object metadata].freeze
7
+ PERIODS = { "1h" => 1.hour, "24h" => 24.hours, "7d" => 7.days }.freeze
7
8
 
8
9
  def self.configure_connection!
9
10
  return unless (opts = RailsAuditLog.connects_to)
@@ -41,8 +42,9 @@ module RailsAuditLog
41
42
  }
42
43
 
43
44
  # Time scopes
44
- scope :since, ->(time) { where(created_at: time..) }
45
- scope :until, ->(time) { where(created_at: ..time) }
45
+ scope :since, ->(time) { where(created_at: time..) }
46
+ scope :until, ->(time) { where(created_at: ..time) }
47
+ scope :for_period, ->(period) { where(created_at: PERIODS[period].ago..) }
46
48
 
47
49
  # Projection scope — omits JSON blob columns for index/listing queries
48
50
  scope :slim, -> { select(column_names - BLOB_COLUMNS) }