rails_audit_log 0.8.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 +10 -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/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/templates/rails_audit_log.rb +10 -0
- data/lib/rails_audit_log/engine.rb +28 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +6 -0
- metadata +62 -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
|
@@ -31,6 +31,16 @@ bin/rails generate rails_audit_log:initializer
|
|
|
31
31
|
|
|
32
32
|
This creates `config/initializers/rails_audit_log.rb` with all settings documented as commented examples inside a `RailsAuditLog.configure` block.
|
|
33
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
|
+
|
|
34
44
|
## Usage
|
|
35
45
|
|
|
36
46
|
### Tracking a model
|
|
@@ -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. */
|
|
@@ -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) }
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
<
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Audit Log</title>
|
|
7
|
+
<link rel="icon" href="data:,">
|
|
5
8
|
<%= csrf_meta_tags %>
|
|
6
9
|
<%= csp_meta_tag %>
|
|
7
|
-
|
|
8
|
-
<%=
|
|
9
|
-
|
|
10
|
-
<%= stylesheet_link_tag "rails_audit_log/application", media: "all" %>
|
|
10
|
+
<%= dashboard_stylesheets %>
|
|
11
|
+
<%= javascript_importmap_tags "rails_audit_log" %>
|
|
11
12
|
</head>
|
|
12
13
|
<body>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
<header class="ral-header">
|
|
15
|
+
<div class="ral-header__inner">
|
|
16
|
+
<%= link_to "Audit Log", root_path, class: "ral-header__logo" %>
|
|
17
|
+
</div>
|
|
18
|
+
</header>
|
|
19
|
+
<main class="ral-main">
|
|
20
|
+
<%= yield %>
|
|
21
|
+
</main>
|
|
16
22
|
</body>
|
|
17
|
-
</html>
|
|
23
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<% changes = entry.object_changes.presence %>
|
|
2
|
+
<% if changes %>
|
|
3
|
+
<div class="ral-diff-wrap" data-controller="diff">
|
|
4
|
+
<div class="ral-diff-toggle">
|
|
5
|
+
<button class="ral-diff-btn ral-diff-btn--active"
|
|
6
|
+
data-diff-target="inlineBtn"
|
|
7
|
+
data-action="click->diff#setInline">Inline</button>
|
|
8
|
+
<button class="ral-diff-btn"
|
|
9
|
+
data-diff-target="sideBtn"
|
|
10
|
+
data-action="click->diff#setSide">Side by side</button>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<table class="ral-diff" data-diff-target="inline">
|
|
14
|
+
<thead>
|
|
15
|
+
<tr>
|
|
16
|
+
<th>Attribute</th>
|
|
17
|
+
<th>Before</th>
|
|
18
|
+
<th>After</th>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
<% changes.each do |attr, (before, after)| %>
|
|
23
|
+
<tr>
|
|
24
|
+
<td class="ral-diff__attr"><%= attr %></td>
|
|
25
|
+
<td class="ral-diff__before"><%= format_diff_value(before) %></td>
|
|
26
|
+
<td class="ral-diff__after"><%= format_diff_value(after) %></td>
|
|
27
|
+
</tr>
|
|
28
|
+
<% end %>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
|
|
32
|
+
<div class="ral-diff-side" data-diff-target="side" hidden>
|
|
33
|
+
<div class="ral-diff-side__panel ral-diff-side__panel--before">
|
|
34
|
+
<div class="ral-diff-side__header">Before</div>
|
|
35
|
+
<% changes.each do |attr, (before, _after)| %>
|
|
36
|
+
<div class="ral-diff-side__row">
|
|
37
|
+
<span class="ral-diff-side__attr"><%= attr %></span>
|
|
38
|
+
<span class="ral-diff-side__value"><%= format_diff_value(before) %></span>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="ral-diff-side__panel ral-diff-side__panel--after">
|
|
43
|
+
<div class="ral-diff-side__header">After</div>
|
|
44
|
+
<% changes.each do |attr, (_before, after)| %>
|
|
45
|
+
<div class="ral-diff-side__row">
|
|
46
|
+
<span class="ral-diff-side__attr"><%= attr %></span>
|
|
47
|
+
<span class="ral-diff-side__value"><%= format_diff_value(after) %></span>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<% else %>
|
|
54
|
+
<p class="ral-muted">No changes recorded.</p>
|
|
55
|
+
<% end %>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<div class="ral-page-header">
|
|
2
|
+
<h1 class="ral-page-header__title">Audit Entries</h1>
|
|
3
|
+
<span class="ral-page-header__meta"><%= @pagy.count %> <%= "entry".pluralize(@pagy.count) %></span>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%= turbo_frame_tag "ral-entries", data: { turbo_action: "advance" } do %>
|
|
7
|
+
<form class="ral-filters" action="<%= audit_log_entries_path %>" method="get"
|
|
8
|
+
data-controller="search">
|
|
9
|
+
<%= hidden_field_tag :period, @period if @period %>
|
|
10
|
+
|
|
11
|
+
<input type="search" name="q" value="<%= @q %>"
|
|
12
|
+
class="ral-filter-input" placeholder="Filter by actor…"
|
|
13
|
+
autocomplete="off" aria-label="Filter by actor"
|
|
14
|
+
data-action="input->search#filter">
|
|
15
|
+
|
|
16
|
+
<select name="event" class="ral-filter-select" aria-label="Filter by event"
|
|
17
|
+
data-action="change->search#select">
|
|
18
|
+
<option value="">All events</option>
|
|
19
|
+
<% RailsAuditLog::AuditLogEntry::EVENTS.each do |event| %>
|
|
20
|
+
<option value="<%= event %>" <%= "selected" if @event == event %>><%= event.capitalize %></option>
|
|
21
|
+
<% end %>
|
|
22
|
+
</select>
|
|
23
|
+
|
|
24
|
+
<% if @item_types.size > 1 %>
|
|
25
|
+
<select name="item_type" class="ral-filter-select" aria-label="Filter by resource"
|
|
26
|
+
data-action="change->search#select">
|
|
27
|
+
<option value="">All resources</option>
|
|
28
|
+
<% @item_types.each do |type| %>
|
|
29
|
+
<option value="<%= type %>" <%= "selected" if @item_type == type %>><%= type %></option>
|
|
30
|
+
<% end %>
|
|
31
|
+
</select>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<div class="ral-period-filter" role="group" aria-label="Time period">
|
|
35
|
+
<%= link_to "All", audit_log_entries_path(event: @event, item_type: @item_type, q: @q),
|
|
36
|
+
class: "ral-period-btn#{" ral-period-btn--active" if @period.blank?}" %>
|
|
37
|
+
<% RailsAuditLog::AuditLogEntry::PERIODS.each_key do |p| %>
|
|
38
|
+
<%= link_to p, audit_log_entries_path(event: @event, item_type: @item_type, q: @q, period: p),
|
|
39
|
+
class: "ral-period-btn#{" ral-period-btn--active" if @period == p}" %>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<% if @event || @item_type || @period || @q %>
|
|
44
|
+
<%= link_to "Clear", audit_log_entries_path, class: "ral-link ral-filters__clear" %>
|
|
45
|
+
<% end %>
|
|
46
|
+
</form>
|
|
47
|
+
|
|
48
|
+
<% if @entries.any? %>
|
|
49
|
+
<div class="ral-table-wrap">
|
|
50
|
+
<table class="ral-table">
|
|
51
|
+
<thead>
|
|
52
|
+
<tr>
|
|
53
|
+
<th>Event</th>
|
|
54
|
+
<th>Resource</th>
|
|
55
|
+
<th>Actor</th>
|
|
56
|
+
<th>When</th>
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
<tbody>
|
|
60
|
+
<% @entries.each do |entry| %>
|
|
61
|
+
<tr>
|
|
62
|
+
<td><span class="ral-badge ral-badge--<%= entry.event %>"><%= entry.event %></span></td>
|
|
63
|
+
<td><%= link_to "#{entry.item_type} ##{entry.item_id}", resource_path(item_type: entry.item_type, item_id: entry.item_id), class: "ral-link", data: { turbo_frame: "_top" } %></td>
|
|
64
|
+
<td><%= entry.whodunnit_snapshot.presence || (entry.actor_type ? "#{entry.actor_type} \##{entry.actor_id}" : "—") %></td>
|
|
65
|
+
<td class="ral-timestamp"><%= entry.created_at.utc.strftime("%Y-%m-%d %H:%M UTC") %></td>
|
|
66
|
+
</tr>
|
|
67
|
+
<% end %>
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
|
|
73
|
+
<% else %>
|
|
74
|
+
<p class="ral-empty">No audit entries found.</p>
|
|
75
|
+
<% end %>
|
|
76
|
+
<% end %>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<div class="ral-page-header">
|
|
2
|
+
<div>
|
|
3
|
+
<p class="ral-breadcrumb">
|
|
4
|
+
<%= link_to "Audit Entries", audit_log_entries_path, class: "ral-link" %> /
|
|
5
|
+
<%= link_to "#{@entry.item_type} ##{@entry.item_id}", resource_path(item_type: @entry.item_type, item_id: @entry.item_id), class: "ral-link" %>
|
|
6
|
+
</p>
|
|
7
|
+
<h1 class="ral-page-header__title">Entry #<%= @entry.id %></h1>
|
|
8
|
+
</div>
|
|
9
|
+
<nav class="ral-entry-nav" aria-label="Entry navigation">
|
|
10
|
+
<% if (prev_entry = @entry.previous) %>
|
|
11
|
+
<%= link_to "← Previous", audit_log_entry_path(prev_entry), class: "ral-pagination__link" %>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% if (next_entry = @entry.next) %>
|
|
14
|
+
<%= link_to "Next →", audit_log_entry_path(next_entry), class: "ral-pagination__link" %>
|
|
15
|
+
<% end %>
|
|
16
|
+
</nav>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="ral-card">
|
|
20
|
+
<dl class="ral-meta">
|
|
21
|
+
<div class="ral-meta__row">
|
|
22
|
+
<dt>Event</dt>
|
|
23
|
+
<dd><span class="ral-badge ral-badge--<%= @entry.event %>"><%= @entry.event %></span></dd>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="ral-meta__row">
|
|
26
|
+
<dt>Resource</dt>
|
|
27
|
+
<dd><%= link_to "#{@entry.item_type} ##{@entry.item_id}", resource_path(item_type: @entry.item_type, item_id: @entry.item_id), class: "ral-link" %></dd>
|
|
28
|
+
</div>
|
|
29
|
+
<% actor = @entry.whodunnit_snapshot.presence || (@entry.actor_type ? "#{@entry.actor_type} \##{@entry.actor_id}" : "—") %>
|
|
30
|
+
<div class="ral-meta__row">
|
|
31
|
+
<dt>Actor</dt>
|
|
32
|
+
<dd><%= actor %></dd>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="ral-meta__row">
|
|
35
|
+
<dt>When</dt>
|
|
36
|
+
<dd class="ral-timestamp"><%= @entry.created_at.utc.strftime("%Y-%m-%d %H:%M:%S UTC") %></dd>
|
|
37
|
+
</div>
|
|
38
|
+
<% if @entry.reason.present? %>
|
|
39
|
+
<div class="ral-meta__row">
|
|
40
|
+
<dt>Reason</dt>
|
|
41
|
+
<dd><%= @entry.reason %></dd>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</dl>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<h2 class="ral-section-title">Changes</h2>
|
|
48
|
+
<div class="ral-card">
|
|
49
|
+
<%= render "diff", entry: @entry %>
|
|
50
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<div class="ral-page-header">
|
|
2
|
+
<div>
|
|
3
|
+
<p class="ral-breadcrumb"><%= link_to "Audit Entries", audit_log_entries_path, class: "ral-link" %></p>
|
|
4
|
+
<h1 class="ral-page-header__title"><%= @item_type %> #<%= @item_id %></h1>
|
|
5
|
+
</div>
|
|
6
|
+
<span class="ral-page-header__meta"><%= @pagy.count %> <%= "entry".pluralize(@pagy.count) %></span>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<%= turbo_frame_tag "ral-resource-entries", data: { turbo_action: "advance" } do %>
|
|
10
|
+
<% if @entries.any? %>
|
|
11
|
+
<div class="ral-timeline">
|
|
12
|
+
<% @entries.each do |entry| %>
|
|
13
|
+
<div class="ral-timeline__entry">
|
|
14
|
+
<div class="ral-timeline__header">
|
|
15
|
+
<span class="ral-badge ral-badge--<%= entry.event %>"><%= entry.event %></span>
|
|
16
|
+
<span class="ral-timestamp"><%= entry.created_at.utc.strftime("%Y-%m-%d %H:%M UTC") %></span>
|
|
17
|
+
<% actor = entry.whodunnit_snapshot.presence || (entry.actor_type ? "#{entry.actor_type} \##{entry.actor_id}" : nil) %>
|
|
18
|
+
<% if actor %>
|
|
19
|
+
<span class="ral-muted">by <%= actor %></span>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% if entry.reason.present? %>
|
|
22
|
+
<span class="ral-reason">"<%= entry.reason %>"</span>
|
|
23
|
+
<% end %>
|
|
24
|
+
<%= link_to "Details →", audit_log_entry_path(entry), class: "ral-link ral-timeline__detail-link", data: { turbo_frame: "_top" } %>
|
|
25
|
+
</div>
|
|
26
|
+
<%= render "rails_audit_log/audit_log_entries/diff", entry: entry %>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
|
|
32
|
+
<% else %>
|
|
33
|
+
<p class="ral-empty">No audit entries for this record.</p>
|
|
34
|
+
<% end %>
|
|
35
|
+
<% end %>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"
|
|
2
|
+
pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
|
|
3
|
+
pin "rails_audit_log", to: "rails_audit_log/application.js"
|
|
4
|
+
pin "rails_audit_log/search_controller", to: "rails_audit_log/search_controller.js"
|
|
5
|
+
pin "rails_audit_log/diff_controller", to: "rails_audit_log/diff_controller.js"
|
data/config/routes.rb
CHANGED
|
@@ -28,4 +28,14 @@ RailsAuditLog.configure do |config|
|
|
|
28
28
|
|
|
29
29
|
# Route AuditLogEntry to a dedicated database (Rails multi-DB). Default: nil
|
|
30
30
|
# config.connects_to = { database: { writing: :audit_log, reading: :audit_log } }
|
|
31
|
+
|
|
32
|
+
# Number of entries per page in the web dashboard. Default: 25
|
|
33
|
+
# config.page_size = 50
|
|
34
|
+
|
|
35
|
+
# Gate web dashboard access. Block runs in controller context — controller
|
|
36
|
+
# methods like current_user are available directly, or accept the controller
|
|
37
|
+
# as an argument. Falls back to HTTP Basic auth if the block returns falsy.
|
|
38
|
+
# Leave unset to allow unauthenticated access (development default).
|
|
39
|
+
# config.authenticate { current_user&.admin? }
|
|
40
|
+
# config.authenticate { |c| c.current_user&.admin? }
|
|
31
41
|
end
|
|
@@ -1,7 +1,35 @@
|
|
|
1
|
+
require "turbo-rails"
|
|
2
|
+
require "importmap-rails"
|
|
3
|
+
require "pagy"
|
|
4
|
+
require "pagy/toolbox/paginators/method"
|
|
5
|
+
|
|
1
6
|
module RailsAuditLog
|
|
2
7
|
class Engine < ::Rails::Engine
|
|
3
8
|
isolate_namespace RailsAuditLog
|
|
4
9
|
|
|
10
|
+
config.i18n.load_path += Gem.find_files("pagy/locales/en.yml")
|
|
11
|
+
|
|
12
|
+
initializer "rails_audit_log.pagy" do |app|
|
|
13
|
+
app.config.after_initialize do
|
|
14
|
+
Pagy::OPTIONS[:limit] = RailsAuditLog.page_size
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
initializer "rails_audit_log.assets" do |app|
|
|
19
|
+
if app.config.respond_to?(:assets)
|
|
20
|
+
app.config.assets.paths << root.join("app/assets/stylesheets")
|
|
21
|
+
app.config.assets.paths << root.join("app/assets/images")
|
|
22
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
initializer "rails_audit_log.importmap", before: "importmap" do |app|
|
|
27
|
+
if app.config.respond_to?(:importmap)
|
|
28
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
29
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
5
33
|
initializer "rails_audit_log.connect_audit_db" do
|
|
6
34
|
ActiveSupport.on_load(:active_record) do
|
|
7
35
|
RailsAuditLog::AuditLogEntry.configure_connection!
|
data/lib/rails_audit_log.rb
CHANGED
|
@@ -14,6 +14,12 @@ module RailsAuditLog
|
|
|
14
14
|
mattr_accessor :version_limit, default: nil
|
|
15
15
|
mattr_accessor :async, default: false
|
|
16
16
|
mattr_accessor :connects_to, default: nil
|
|
17
|
+
mattr_accessor :page_size, default: 25
|
|
18
|
+
|
|
19
|
+
def self.authenticate(&block)
|
|
20
|
+
@authenticate = block if block_given?
|
|
21
|
+
@authenticate
|
|
22
|
+
end
|
|
17
23
|
mattr_accessor :whodunnit_display, default: ->(actor) {
|
|
18
24
|
actor.respond_to?(:name) ? actor.name.to_s : actor.to_s
|
|
19
25
|
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_audit_log
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -23,6 +23,48 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '7.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: turbo-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: importmap-rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.2'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.2'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: pagy
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '43.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '43.0'
|
|
26
68
|
description: A modern Rails engine that tracks ActiveRecord create, update, and destroy
|
|
27
69
|
events with JSON-first storage, whodunnit actor context, and a clean query API.
|
|
28
70
|
Drop-in replacement for PaperTrail with no legacy baggage.
|
|
@@ -35,16 +77,35 @@ files:
|
|
|
35
77
|
- MIT-LICENSE
|
|
36
78
|
- README.md
|
|
37
79
|
- Rakefile
|
|
80
|
+
- app/assets/stylesheets/rails_audit_log/_01_base.css
|
|
81
|
+
- app/assets/stylesheets/rails_audit_log/_02_layout.css
|
|
82
|
+
- app/assets/stylesheets/rails_audit_log/_03_table.css
|
|
83
|
+
- app/assets/stylesheets/rails_audit_log/_04_badges.css
|
|
84
|
+
- app/assets/stylesheets/rails_audit_log/_05_diff.css
|
|
85
|
+
- app/assets/stylesheets/rails_audit_log/_06_timeline.css
|
|
86
|
+
- app/assets/stylesheets/rails_audit_log/_07_detail.css
|
|
87
|
+
- app/assets/stylesheets/rails_audit_log/_08_pagination.css
|
|
88
|
+
- app/assets/stylesheets/rails_audit_log/_09_filters.css
|
|
38
89
|
- app/assets/stylesheets/rails_audit_log/application.css
|
|
39
90
|
- app/concerns/rails_audit_log/auditable.rb
|
|
40
91
|
- app/concerns/rails_audit_log/controller.rb
|
|
41
92
|
- app/controllers/rails_audit_log/application_controller.rb
|
|
93
|
+
- app/controllers/rails_audit_log/audit_log_entries_controller.rb
|
|
94
|
+
- app/controllers/rails_audit_log/resources_controller.rb
|
|
42
95
|
- app/helpers/rails_audit_log/application_helper.rb
|
|
96
|
+
- app/javascript/rails_audit_log/application.js
|
|
97
|
+
- app/javascript/rails_audit_log/diff_controller.js
|
|
98
|
+
- app/javascript/rails_audit_log/search_controller.js
|
|
43
99
|
- app/jobs/rails_audit_log/application_job.rb
|
|
44
100
|
- app/jobs/rails_audit_log/write_audit_log_job.rb
|
|
45
101
|
- app/models/rails_audit_log/application_record.rb
|
|
46
102
|
- app/models/rails_audit_log/audit_log_entry.rb
|
|
47
103
|
- app/views/layouts/rails_audit_log/application.html.erb
|
|
104
|
+
- app/views/rails_audit_log/audit_log_entries/_diff.html.erb
|
|
105
|
+
- app/views/rails_audit_log/audit_log_entries/index.html.erb
|
|
106
|
+
- app/views/rails_audit_log/audit_log_entries/show.html.erb
|
|
107
|
+
- app/views/rails_audit_log/resources/show.html.erb
|
|
108
|
+
- config/importmap.rb
|
|
48
109
|
- config/routes.rb
|
|
49
110
|
- lib/generators/rails_audit_log/initializer/initializer_generator.rb
|
|
50
111
|
- lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb
|