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.
- checksums.yaml +4 -4
- data/README.md +88 -0
- data/app/assets/stylesheets/rails_audit_log/_01_base.css +32 -0
- data/app/assets/stylesheets/rails_audit_log/_02_layout.css +34 -0
- data/app/assets/stylesheets/rails_audit_log/_03_table.css +39 -0
- data/app/assets/stylesheets/rails_audit_log/_04_badges.css +13 -0
- data/app/assets/stylesheets/rails_audit_log/_05_diff.css +94 -0
- data/app/assets/stylesheets/rails_audit_log/_06_timeline.css +21 -0
- data/app/assets/stylesheets/rails_audit_log/_07_detail.css +15 -0
- data/app/assets/stylesheets/rails_audit_log/_08_pagination.css +56 -0
- data/app/assets/stylesheets/rails_audit_log/_09_filters.css +54 -0
- data/app/assets/stylesheets/rails_audit_log/application.css +1 -15
- data/app/concerns/rails_audit_log/auditable.rb +14 -0
- data/app/controllers/rails_audit_log/application_controller.rb +12 -0
- data/app/controllers/rails_audit_log/audit_log_entries_controller.rb +31 -0
- data/app/controllers/rails_audit_log/resources_controller.rb +13 -0
- data/app/helpers/rails_audit_log/application_helper.rb +13 -0
- data/app/javascript/rails_audit_log/application.js +8 -0
- data/app/javascript/rails_audit_log/diff_controller.js +20 -0
- data/app/javascript/rails_audit_log/search_controller.js +12 -0
- data/app/models/rails_audit_log/audit_log_entry.rb +5 -3
- data/app/views/layouts/rails_audit_log/application.html.erb +16 -10
- data/app/views/rails_audit_log/audit_log_entries/_diff.html.erb +55 -0
- data/app/views/rails_audit_log/audit_log_entries/index.html.erb +76 -0
- data/app/views/rails_audit_log/audit_log_entries/show.html.erb +50 -0
- data/app/views/rails_audit_log/resources/show.html.erb +35 -0
- data/config/importmap.rb +5 -0
- data/config/routes.rb +3 -0
- data/lib/generators/rails_audit_log/initializer/initializer_generator.rb +15 -0
- data/lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb +41 -0
- data/lib/rails_audit_log/engine.rb +28 -0
- data/lib/rails_audit_log/matchers.rb +103 -0
- data/lib/rails_audit_log/minitest_assertions.rb +31 -0
- data/lib/rails_audit_log/test_helpers.rb +7 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +10 -0
- metadata +67 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c9a9fcdb866f5300a3a2fd9bd6026ad93530d15b0b26d771ef1a28a47985280
|
|
4
|
+
data.tar.gz: 01ac4b3f95fd1c2067a8aa5936122ff86dc264fe771881515dd31d3401377b66
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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,
|
|
45
|
-
scope :until,
|
|
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) }
|