solid_stack_web 1.5.0 → 1.6.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 +157 -3
- data/app/assets/stylesheets/solid_stack_web/_02_layout.css +12 -0
- data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +4 -3
- data/app/controllers/solid_stack_web/application_controller.rb +10 -0
- data/app/controllers/solid_stack_web/audit_controller.rb +1 -1
- data/app/controllers/solid_stack_web/cable/channel_purges_controller.rb +1 -1
- data/app/controllers/solid_stack_web/cable/purges_controller.rb +1 -1
- data/app/controllers/solid_stack_web/cache/flushes_controller.rb +1 -1
- data/app/controllers/solid_stack_web/cache_entries_controller.rb +1 -1
- data/app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb +3 -3
- data/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +4 -4
- data/app/controllers/solid_stack_web/failed_jobs_controller.rb +2 -2
- data/app/controllers/solid_stack_web/jobs/selections_controller.rb +2 -2
- data/app/controllers/solid_stack_web/jobs_controller.rb +2 -2
- data/app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb +4 -4
- data/app/controllers/solid_stack_web/scheduled_jobs_controller.rb +5 -5
- data/app/helpers/solid_stack_web/application_helper.rb +30 -17
- data/app/views/layouts/solid_stack_web/application.html.erb +28 -24
- data/app/views/solid_stack_web/audit/index.html.erb +18 -18
- data/app/views/solid_stack_web/cable/index.html.erb +22 -19
- data/app/views/solid_stack_web/cable_messages/index.html.erb +15 -14
- data/app/views/solid_stack_web/cache/index.html.erb +19 -19
- data/app/views/solid_stack_web/cache_entries/index.html.erb +16 -15
- data/app/views/solid_stack_web/cache_entries/show.html.erb +11 -11
- data/app/views/solid_stack_web/dashboard/index.html.erb +54 -33
- data/app/views/solid_stack_web/errors/internal_server_error.html.erb +4 -4
- data/app/views/solid_stack_web/errors/not_found.html.erb +4 -4
- data/app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb +2 -2
- data/app/views/solid_stack_web/failed_jobs/errors/index.html.erb +10 -10
- data/app/views/solid_stack_web/failed_jobs/index.html.erb +22 -22
- data/app/views/solid_stack_web/failed_jobs/show.html.erb +16 -16
- data/app/views/solid_stack_web/history/index.html.erb +19 -18
- data/app/views/solid_stack_web/jobs/_empty.html.erb +8 -8
- data/app/views/solid_stack_web/jobs/index.html.erb +35 -34
- data/app/views/solid_stack_web/jobs/show.html.erb +16 -16
- data/app/views/solid_stack_web/processes/index.html.erb +8 -8
- data/app/views/solid_stack_web/queues/index.html.erb +13 -13
- data/app/views/solid_stack_web/queues/show.html.erb +16 -16
- data/app/views/solid_stack_web/recurring_tasks/index.html.erb +16 -16
- data/app/views/solid_stack_web/shared/_locale_switcher.html.erb +14 -0
- data/app/views/solid_stack_web/stats/index.html.erb +13 -13
- data/config/locales/en.yml +395 -0
- data/config/locales/es.yml +395 -0
- data/lib/solid_stack_web/engine.rb +1 -0
- data/lib/solid_stack_web/version.rb +1 -1
- data/lib/solid_stack_web.rb +14 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db27129d1f850266817f633ff0109fdd3708c7105fdea6d0bcf3035ec1734769
|
|
4
|
+
data.tar.gz: 1549319d21ac55da226b2ce4487a8a8a1340531b13c838643fb2754843620518
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '02499cb54b94ad58e62204d18f60780cc88e22c62706248f5fb60a4905f36eb822e8243de0351df15a915f740501bde0b973fa9e6a17754ba2741b64f4b6b695'
|
|
7
|
+
data.tar.gz: 627aae0342fa4f93ef0a6e25b89e56764ff6bda2e2c75d7b0315675ee2daacdc2e57ed428698485302b2a6950725bf5d63bb5c5c51a6e566d55f141cbf820c08
|
data/README.md
CHANGED
|
@@ -1,12 +1,48 @@
|
|
|
1
1
|
# SolidStackWeb
|
|
2
2
|
|
|
3
3
|
[](https://github.com/eclectic-coding/solid_stack_web/actions/workflows/ci.yml)
|
|
4
|
-
[](https://rubygems.org/gems/solid_stack_web)
|
|
5
5
|
[](https://rubygems.org/gems/solid_stack_web)
|
|
6
6
|
[](https://www.ruby-lang.org)
|
|
7
7
|
[](https://codecov.io/gh/eclectic-coding/solid_stack_web)
|
|
8
8
|
|
|
9
|
-
A production-ready operations dashboard for the full Rails Solid Stack. Mount one engine to get deep visibility into **Solid Queue** (job browser, failed job retry, queue controls, recurring tasks, performance stats), **Solid Cache** (entry browser, size distribution, write timeline), and **Solid Cable** (channel browser, message list, purge controls) — with dark mode, CSV export, alert webhooks, and a JSON metrics endpoint, all with no asset pipeline dependency.
|
|
9
|
+
A production-ready operations dashboard for the full Rails Solid Stack. Mount one engine to get deep visibility into **Solid Queue** (job browser, failed job retry, queue controls, recurring tasks, performance stats), **Solid Cache** (entry browser, size distribution, write timeline), and **Solid Cable** (channel browser, message list, purge controls) — with dark mode, i18n locale switching, custom nav links, custom dashboard cards, CSV export, alert webhooks, and a JSON metrics endpoint, all with no asset pipeline dependency.
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Screenshots](#screenshots)
|
|
15
|
+
- [Metrics endpoint](#metrics-endpoint)
|
|
16
|
+
- [General configuration](#general-configuration)
|
|
17
|
+
- [Authentication](#authentication)
|
|
18
|
+
- [Linking to the dashboard](#linking-to-the-dashboard)
|
|
19
|
+
- [i18n](#i18n)
|
|
20
|
+
- [Adding a custom locale](#adding-a-custom-locale)
|
|
21
|
+
- [Extensibility](#extensibility)
|
|
22
|
+
- [Custom nav links](#custom-nav-links)
|
|
23
|
+
- [Custom dashboard cards](#custom-dashboard-cards)
|
|
24
|
+
- [Security](#security)
|
|
25
|
+
- [Authentication](#authentication-1)
|
|
26
|
+
- [Sensitive cache values](#sensitive-cache-values)
|
|
27
|
+
- [CSRF protection](#csrf-protection)
|
|
28
|
+
- [Rate limiting and network exposure](#rate-limiting-and-network-exposure)
|
|
29
|
+
- [Solid Queue](#solid-queue)
|
|
30
|
+
- [Features](#features)
|
|
31
|
+
- [Configuration](#configuration)
|
|
32
|
+
- [Job Filtering](#job-filtering)
|
|
33
|
+
- [Solid Cache](#solid-cache)
|
|
34
|
+
- [Features](#features-1)
|
|
35
|
+
- [Solid Cable](#solid-cable)
|
|
36
|
+
- [Features](#features-2)
|
|
37
|
+
- [Requirements](#requirements)
|
|
38
|
+
- [Versioning](#versioning)
|
|
39
|
+
- [Public API](#public-api)
|
|
40
|
+
- [Not part of the public API](#not-part-of-the-public-api)
|
|
41
|
+
- [Deprecation policy](#deprecation-policy)
|
|
42
|
+
- [Contributing](#contributing)
|
|
43
|
+
- [License](#license)
|
|
44
|
+
|
|
45
|
+
---
|
|
10
46
|
|
|
11
47
|
## Installation
|
|
12
48
|
|
|
@@ -30,12 +66,16 @@ rails generate solid_stack_web:install
|
|
|
30
66
|
|
|
31
67
|
This creates `config/initializers/solid_stack_web.rb` with every configuration option commented inline, and injects `mount SolidStackWeb::Engine, at: "/solid_stack"` into `config/routes.rb`. The dashboard will then be available at `/solid_stack` (or whatever path you choose).
|
|
32
68
|
|
|
69
|
+
[↑ Back to top](#table-of-contents)
|
|
70
|
+
|
|
33
71
|
---
|
|
34
72
|
|
|
35
73
|
## Screenshots
|
|
36
74
|
|
|
37
75
|

|
|
38
76
|
|
|
77
|
+
[↑ Back to top](#table-of-contents)
|
|
78
|
+
|
|
39
79
|
---
|
|
40
80
|
|
|
41
81
|
## Metrics endpoint
|
|
@@ -64,6 +104,8 @@ This creates `config/initializers/solid_stack_web.rb` with every configuration o
|
|
|
64
104
|
|
|
65
105
|
`slow_jobs` is only present when `slow_job_threshold` is configured. The endpoint is protected by the same authentication as the rest of the dashboard.
|
|
66
106
|
|
|
107
|
+
[↑ Back to top](#table-of-contents)
|
|
108
|
+
|
|
67
109
|
---
|
|
68
110
|
|
|
69
111
|
## General configuration
|
|
@@ -84,6 +126,12 @@ SolidStackWeb.configure do |config|
|
|
|
84
126
|
# Multi-database — pass a connects_to hash when Solid Queue / Cache / Cable
|
|
85
127
|
# live on a separate database from your primary (default: nil, uses primary).
|
|
86
128
|
config.connects_to = { database: { writing: :queue, reading: :queue } }
|
|
129
|
+
|
|
130
|
+
# Custom nav links — appended to the main navigation bar (default: []).
|
|
131
|
+
config.nav_links = [{ label: "Admin", url: "/admin" }]
|
|
132
|
+
|
|
133
|
+
# Custom dashboard cards — rendered after the built-in cards (default: []).
|
|
134
|
+
config.dashboard_cards = [{ title: "My App", stats: -> { { "Users" => User.count } } }]
|
|
87
135
|
end
|
|
88
136
|
```
|
|
89
137
|
|
|
@@ -99,6 +147,85 @@ The `authenticate` block is evaluated in the context of each request's controlle
|
|
|
99
147
|
link_to "Queue Dashboard", SolidStackWeb.mount_path
|
|
100
148
|
```
|
|
101
149
|
|
|
150
|
+
[↑ Back to top](#table-of-contents)
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## i18n
|
|
155
|
+
|
|
156
|
+
All dashboard UI strings — page titles, table headers, button labels, empty states, flash messages, and sparkline tooltips — are backed by locale YAML files. The engine ships with **English (`en`)** and **Spanish (`es`)** built in.
|
|
157
|
+
|
|
158
|
+
A language selector appears in the dashboard header and lets users switch locales at runtime. The selected locale is stored in the session and applied via `I18n.with_locale`, so it persists across requests without touching the host application's locale. The `?locale=` query param takes precedence over the session value, making it easy to deep-link to a specific language.
|
|
159
|
+
|
|
160
|
+
The switcher is automatically hidden when `config.available_locales` contains only one entry.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
SolidStackWeb.configure do |config|
|
|
164
|
+
# Locales shown in the language switcher (default: [:en, :es]).
|
|
165
|
+
# Set to [:en] to hide the switcher entirely.
|
|
166
|
+
config.available_locales = [:en, :es]
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Adding a custom locale
|
|
171
|
+
|
|
172
|
+
1. Create a locale file in your host application under `config/locales/`, e.g. `config/locales/solid_stack_web.fr.yml`.
|
|
173
|
+
2. Nest all keys under `fr > solid_stack_web:` — use `config/locales/en.yml` in the gem as a reference for the full key list.
|
|
174
|
+
3. Add the locale symbol to `config.available_locales`:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
config.available_locales = [:en, :es, :fr]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Rails will pick up the file automatically via its standard `config.i18n.load_path`; no additional configuration is needed.
|
|
181
|
+
|
|
182
|
+
[↑ Back to top](#table-of-contents)
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Extensibility
|
|
187
|
+
|
|
188
|
+
### Custom nav links
|
|
189
|
+
|
|
190
|
+
`config.nav_links` appends extra links to the main navigation bar after the built-in Queue / Cache / Cable links. Use it to link back to your host application's admin pages or related tools without modifying the engine layout.
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
SolidStackWeb.configure do |config|
|
|
194
|
+
config.nav_links = [
|
|
195
|
+
{ label: "Back to App", url: "/" },
|
|
196
|
+
{ label: "Admin", url: "/admin" }
|
|
197
|
+
]
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Defaults to `[]` — no extra links appear when unconfigured.
|
|
202
|
+
|
|
203
|
+
### Custom dashboard cards
|
|
204
|
+
|
|
205
|
+
`config.dashboard_cards` adds custom stat cards to the overview dashboard after the built-in Queue, Cache, and Cable cards. Each card accepts three keys:
|
|
206
|
+
|
|
207
|
+
| Key | Type | Description |
|
|
208
|
+
|-----|------|-------------|
|
|
209
|
+
| `title` | String | Card heading (required) |
|
|
210
|
+
| `link` | `{ label:, url: }` | Optional header link rendered top-right |
|
|
211
|
+
| `stats` | Lambda | Optional — called at render time; must return a `{ label => value }` hash |
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
SolidStackWeb.configure do |config|
|
|
215
|
+
config.dashboard_cards = [
|
|
216
|
+
{
|
|
217
|
+
title: "My App",
|
|
218
|
+
link: { label: "View Admin", url: "/admin" },
|
|
219
|
+
stats: -> { { "Users" => User.count, "Premium" => User.premium.count } }
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The `stats` lambda runs on every dashboard render, so keep it fast. Defaults to `[]` — no custom cards appear when unconfigured.
|
|
226
|
+
|
|
227
|
+
[↑ Back to top](#table-of-contents)
|
|
228
|
+
|
|
102
229
|
---
|
|
103
230
|
|
|
104
231
|
## Security
|
|
@@ -137,6 +264,8 @@ The dashboard is designed to be mounted behind your application's existing authe
|
|
|
137
264
|
- Restricting access by IP at the reverse-proxy level
|
|
138
265
|
- Applying [Rack::Attack](https://github.com/rack/rack-attack) rules to the mount path
|
|
139
266
|
|
|
267
|
+
[↑ Back to top](#table-of-contents)
|
|
268
|
+
|
|
140
269
|
---
|
|
141
270
|
|
|
142
271
|
## Solid Queue
|
|
@@ -159,6 +288,7 @@ The dashboard is designed to be mounted behind your application's existing authe
|
|
|
159
288
|
- **Turbo Stream** job discard — removes the row inline without a full page reload
|
|
160
289
|
- **Sticky filter preferences** — last-used status, period, and queue filter saved to `localStorage`; a fresh visit to the jobs or history list with no URL params automatically restores the previous selection
|
|
161
290
|
- **Dark mode** — toggle button in the header switches between light and dark palettes; preference persisted in `localStorage`; respects `prefers-color-scheme` on first visit
|
|
291
|
+
- **i18n / locale switching** — all UI strings backed by locale YAML files; ships with English (`en`) and Spanish (`es`); a language selector in the header lets users switch at runtime; locale is stored in the session and persists across requests; configure which locales appear via `config.available_locales`
|
|
162
292
|
- **Responsive layout** — stats cards, tables, and two-column grids adapt to narrow viewports; tables scroll horizontally rather than overflow; split page headers stack on small screens
|
|
163
293
|
- **Empty-state improvements** — all list views show a contextual title and an actionable hint; search empty states include a "Clear search" link; filters-active history view offers "Clear filters"; processes and recurring tasks explain the next step
|
|
164
294
|
- **Inline notifications** — bulk and single-job actions surface a flash notice; Turbo Stream discard responses inject the message inline without a full page reload; bulk actions report the affected count ("3 jobs discarded")
|
|
@@ -192,6 +322,12 @@ SolidStackWeb.configure do |config|
|
|
|
192
322
|
# Show the raw serialized value on the cache entry detail page (default: false).
|
|
193
323
|
# Disable for stores that contain sensitive data.
|
|
194
324
|
config.allow_value_preview = true
|
|
325
|
+
|
|
326
|
+
# Locales shown in the language switcher (default: [:en, :es]).
|
|
327
|
+
# The switcher is hidden when only one locale is configured.
|
|
328
|
+
# To add a locale, provide a locale YAML file under your app's config/locales/
|
|
329
|
+
# with keys nested under solid_stack_web:, then add the locale symbol here.
|
|
330
|
+
config.available_locales = [:en, :es]
|
|
195
331
|
end
|
|
196
332
|
```
|
|
197
333
|
|
|
@@ -208,6 +344,8 @@ The jobs list supports four independent filters, all driven by query params:
|
|
|
208
344
|
|
|
209
345
|
Filters are preserved when switching between status tabs (Ready / Scheduled / Running / Blocked) and when discarding a job. They can be combined freely.
|
|
210
346
|
|
|
347
|
+
[↑ Back to top](#table-of-contents)
|
|
348
|
+
|
|
211
349
|
---
|
|
212
350
|
|
|
213
351
|
## Solid Cache
|
|
@@ -224,6 +362,8 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
|
|
|
224
362
|
- **Delete entry** — per-row delete button or detail-page button removes a single cache entry
|
|
225
363
|
- **Flush All** — header button deletes every cache entry with a confirmation prompt
|
|
226
364
|
|
|
365
|
+
[↑ Back to top](#table-of-contents)
|
|
366
|
+
|
|
227
367
|
---
|
|
228
368
|
|
|
229
369
|
## Solid Cable
|
|
@@ -236,6 +376,8 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
|
|
|
236
376
|
- **Per-channel message list** — `GET /cable/channels/:channel_hash` shows a paginated, reverse-chronological list of that channel's `SolidCable::Message` records; each row shows the message ID, a truncated payload preview (120 chars) with the full payload on hover, and a relative sent time with the exact timestamp on hover; supports `?q=` filtering by payload substring; **Purge Channel** button deletes all messages for the channel
|
|
237
377
|
- **Message purge** — "Purge Old" form on the channel browser deletes all messages older than 1, 7, or 30 days; confirmation prompt before any destructive action
|
|
238
378
|
|
|
379
|
+
[↑ Back to top](#table-of-contents)
|
|
380
|
+
|
|
239
381
|
---
|
|
240
382
|
|
|
241
383
|
## Requirements
|
|
@@ -248,6 +390,10 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
|
|
|
248
390
|
- [turbo-rails](https://github.com/hotwired/turbo-rails) >= 2.0
|
|
249
391
|
- [importmap-rails](https://github.com/rails/importmap-rails) >= 1.2
|
|
250
392
|
|
|
393
|
+
[↑ Back to top](#table-of-contents)
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
251
397
|
## Versioning
|
|
252
398
|
|
|
253
399
|
SolidStackWeb follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
@@ -276,6 +422,8 @@ The following are internal and may change in any release without notice:
|
|
|
276
422
|
|
|
277
423
|
When a public API item is renamed or removed, the old interface is deprecated in a **minor** release — it continues to work but issues an `ActiveSupport::Deprecation` warning pointing to the replacement. The old interface is removed in the next **major** release. The [UPGRADING.md](UPGRADING.md) file documents every breaking change and the migration steps.
|
|
278
424
|
|
|
425
|
+
[↑ Back to top](#table-of-contents)
|
|
426
|
+
|
|
279
427
|
---
|
|
280
428
|
|
|
281
429
|
## Contributing
|
|
@@ -287,6 +435,12 @@ When a public API item is renamed or removed, the old interface is deprecated in
|
|
|
287
435
|
|
|
288
436
|
Bug reports and feature requests are welcome on [GitHub Issues](https://github.com/eclectic-coding/solid_stack_web/issues).
|
|
289
437
|
|
|
438
|
+
[↑ Back to top](#table-of-contents)
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
290
442
|
## License
|
|
291
443
|
|
|
292
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
444
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
445
|
+
|
|
446
|
+
[↑ Back to top](#table-of-contents)
|
|
@@ -117,3 +117,15 @@
|
|
|
117
117
|
}
|
|
118
118
|
.sqw-flash--notice { background: #d1e7dd; color: #0a3622; border: 1px solid #a3cfbb; }
|
|
119
119
|
.sqw-flash--alert { background: #f8d7da; color: #58151c; border: 1px solid #f1aeb5; }
|
|
120
|
+
|
|
121
|
+
.sqw-locale-form { display: flex; align-items: center; flex-shrink: 0; }
|
|
122
|
+
.sqw-locale-select {
|
|
123
|
+
font-size: 12px;
|
|
124
|
+
padding: 0.2rem 0.4rem;
|
|
125
|
+
border: 1px solid var(--border);
|
|
126
|
+
border-radius: var(--radius);
|
|
127
|
+
background: var(--surface);
|
|
128
|
+
color: var(--muted);
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
height: 28px;
|
|
131
|
+
}
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
}
|
|
17
17
|
.sqw-gem-card:hover { box-shadow: 0 3px 8px rgba(0,0,0,.12); }
|
|
18
18
|
|
|
19
|
-
.sqw-gem-card--queue
|
|
20
|
-
.sqw-gem-card--cache
|
|
21
|
-
.sqw-gem-card--cable
|
|
19
|
+
.sqw-gem-card--queue { border-top-color: var(--primary); }
|
|
20
|
+
.sqw-gem-card--cache { border-top-color: var(--purple); }
|
|
21
|
+
.sqw-gem-card--cable { border-top-color: var(--info); }
|
|
22
|
+
.sqw-gem-card--custom { border-top-color: var(--muted); }
|
|
22
23
|
|
|
23
24
|
.sqw-gem-card__header {
|
|
24
25
|
display: flex;
|
|
@@ -7,6 +7,7 @@ module SolidStackWeb
|
|
|
7
7
|
PERIOD_DURATIONS = { "1h" => 1.hour, "24h" => 24.hours, "7d" => 7.days }.freeze
|
|
8
8
|
|
|
9
9
|
before_action :authenticate!
|
|
10
|
+
around_action :with_locale
|
|
10
11
|
around_action :with_database_connection
|
|
11
12
|
|
|
12
13
|
rescue_from StandardError do |exception|
|
|
@@ -29,6 +30,15 @@ module SolidStackWeb
|
|
|
29
30
|
end
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
def with_locale
|
|
34
|
+
available = SolidStackWeb.available_locales.map(&:to_s)
|
|
35
|
+
locale = params[:locale].presence_in(available) ||
|
|
36
|
+
session[:solid_stack_web_locale].presence_in(available) ||
|
|
37
|
+
I18n.default_locale.to_s
|
|
38
|
+
session[:solid_stack_web_locale] = locale
|
|
39
|
+
I18n.with_locale(locale) { yield }
|
|
40
|
+
end
|
|
41
|
+
|
|
32
42
|
def with_database_connection
|
|
33
43
|
config = SolidStackWeb.connects_to
|
|
34
44
|
return yield unless config
|
|
@@ -3,7 +3,7 @@ module SolidStackWeb
|
|
|
3
3
|
def index
|
|
4
4
|
unless AuditEvent.table_exists?
|
|
5
5
|
redirect_to root_path,
|
|
6
|
-
alert: "
|
|
6
|
+
alert: t("solid_stack_web.flash.audit_migration_required")
|
|
7
7
|
return
|
|
8
8
|
end
|
|
9
9
|
|
|
@@ -2,7 +2,7 @@ module SolidStackWeb
|
|
|
2
2
|
class Cable::ChannelPurgesController < ApplicationController
|
|
3
3
|
def destroy
|
|
4
4
|
::SolidCable::Message.where(channel_hash: params[:channel_hash]).delete_all
|
|
5
|
-
redirect_to cable_path, notice: "
|
|
5
|
+
redirect_to cable_path, notice: t("solid_stack_web.flash.channel_purged")
|
|
6
6
|
end
|
|
7
7
|
end
|
|
8
8
|
end
|
|
@@ -3,7 +3,7 @@ module SolidStackWeb
|
|
|
3
3
|
def destroy
|
|
4
4
|
days = [params[:older_than].to_i, 1].max
|
|
5
5
|
::SolidCable::Message.where("created_at < ?", days.days.ago).delete_all
|
|
6
|
-
redirect_to cable_path, notice: "
|
|
6
|
+
redirect_to cable_path, notice: t("solid_stack_web.flash.messages_purged", count: days)
|
|
7
7
|
end
|
|
8
8
|
end
|
|
9
9
|
end
|
|
@@ -2,7 +2,7 @@ module SolidStackWeb
|
|
|
2
2
|
class Cache::FlushesController < ApplicationController
|
|
3
3
|
def destroy
|
|
4
4
|
::SolidCache::Entry.delete_all
|
|
5
|
-
redirect_to cache_entries_path, notice: "
|
|
5
|
+
redirect_to cache_entries_path, notice: t("solid_stack_web.flash.cache_flushed")
|
|
6
6
|
end
|
|
7
7
|
end
|
|
8
8
|
end
|
|
@@ -18,7 +18,7 @@ module SolidStackWeb
|
|
|
18
18
|
def destroy
|
|
19
19
|
::SolidCache::Entry.find(params[:id]).destroy
|
|
20
20
|
redirect_to cache_entries_path(q: params[:q], column: params[:column], direction: params[:direction]),
|
|
21
|
-
notice: "
|
|
21
|
+
notice: t("solid_stack_web.flash.cache_entry_deleted")
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
private
|
|
@@ -6,11 +6,11 @@ module SolidStackWeb
|
|
|
6
6
|
new_arguments = JSON.parse(params[:arguments])
|
|
7
7
|
@execution.job.update!(arguments: new_arguments)
|
|
8
8
|
@execution.retry
|
|
9
|
-
redirect_to failed_jobs_path, notice: "
|
|
9
|
+
redirect_to failed_jobs_path, notice: t("solid_stack_web.flash.arguments_updated")
|
|
10
10
|
rescue JSON::ParserError
|
|
11
|
-
redirect_to failed_job_path(@execution), alert: "
|
|
11
|
+
redirect_to failed_job_path(@execution), alert: t("solid_stack_web.flash.invalid_json")
|
|
12
12
|
rescue => e
|
|
13
|
-
redirect_to failed_jobs_path, alert: "
|
|
13
|
+
redirect_to failed_jobs_path, alert: t("solid_stack_web.flash.cannot_update_job", error: e.message)
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
@@ -7,18 +7,18 @@ module SolidStackWeb
|
|
|
7
7
|
count = @ids.size
|
|
8
8
|
SolidQueue::FailedExecution.where(id: @ids).each(&:retry)
|
|
9
9
|
record_audit("failed_jobs_retried", item_count: count)
|
|
10
|
-
redirect_to failed_jobs_path, notice: "
|
|
10
|
+
redirect_to failed_jobs_path, notice: t("solid_stack_web.flash.jobs_retried", count: count)
|
|
11
11
|
rescue => e
|
|
12
|
-
redirect_to failed_jobs_path, alert: "
|
|
12
|
+
redirect_to failed_jobs_path, alert: t("solid_stack_web.flash.cannot_retry_jobs", error: e.message)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def destroy
|
|
16
16
|
job_ids = SolidQueue::FailedExecution.where(id: @ids).pluck(:job_id)
|
|
17
17
|
count = SolidQueue::Job.where(id: job_ids).destroy_all.size
|
|
18
18
|
record_audit("failed_jobs_discarded", item_count: count)
|
|
19
|
-
redirect_to failed_jobs_path, notice: "
|
|
19
|
+
redirect_to failed_jobs_path, notice: t("solid_stack_web.flash.jobs_discarded", count: count)
|
|
20
20
|
rescue => e
|
|
21
|
-
redirect_to failed_jobs_path, alert: "
|
|
21
|
+
redirect_to failed_jobs_path, alert: t("solid_stack_web.flash.cannot_discard_jobs", error: e.message)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
private
|
|
@@ -33,7 +33,7 @@ module SolidStackWeb
|
|
|
33
33
|
@execution.job.destroy!
|
|
34
34
|
record_audit("failed_job_discarded", job_class: job_class, queue_name: queue_name)
|
|
35
35
|
@executions_remain = ::SolidQueue::FailedExecution.exists?
|
|
36
|
-
@notice = "
|
|
36
|
+
@notice = t("solid_stack_web.flash.job_discarded")
|
|
37
37
|
|
|
38
38
|
respond_to do |format|
|
|
39
39
|
format.html { redirect_to failed_jobs_path }
|
|
@@ -45,7 +45,7 @@ module SolidStackWeb
|
|
|
45
45
|
execution = ::SolidQueue::FailedExecution.find(params[:id])
|
|
46
46
|
record_audit("failed_job_retried", job_class: execution.job.class_name, queue_name: execution.job.queue_name)
|
|
47
47
|
execution.retry
|
|
48
|
-
redirect_to failed_jobs_path, notice: "
|
|
48
|
+
redirect_to failed_jobs_path, notice: t("solid_stack_web.flash.job_retried")
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
private
|
|
@@ -3,7 +3,7 @@ module SolidStackWeb
|
|
|
3
3
|
class SelectionsController < ApplicationController
|
|
4
4
|
def destroy
|
|
5
5
|
status = params[:status].presence_in(Job::STATUSES) || "ready"
|
|
6
|
-
raise ArgumentError, "
|
|
6
|
+
raise ArgumentError, t("solid_stack_web.flash.cannot_discard", status: status) unless Job::DISCARDABLE.include?(status)
|
|
7
7
|
|
|
8
8
|
ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
|
|
9
9
|
job_ids = Job::EXECUTION_MODELS[status].where(id: ids).pluck(:job_id)
|
|
@@ -18,7 +18,7 @@ module SolidStackWeb
|
|
|
18
18
|
priority: params[:priority].presence,
|
|
19
19
|
sort: params[:sort].presence,
|
|
20
20
|
direction: params[:direction].presence
|
|
21
|
-
), notice: "
|
|
21
|
+
), notice: t("solid_stack_web.flash.jobs_discarded", count: count)
|
|
22
22
|
rescue ArgumentError => e
|
|
23
23
|
redirect_to jobs_path(status: params[:status]), alert: e.message
|
|
24
24
|
end
|
|
@@ -36,7 +36,7 @@ module SolidStackWeb
|
|
|
36
36
|
@execution.job.destroy!
|
|
37
37
|
record_audit("job_discarded", job_class: job_class, queue_name: queue_name)
|
|
38
38
|
@executions_remain = Job::EXECUTION_MODELS[@status].exists?
|
|
39
|
-
@notice = "
|
|
39
|
+
@notice = t("solid_stack_web.flash.job_discarded")
|
|
40
40
|
|
|
41
41
|
respond_to do |format|
|
|
42
42
|
format.html { redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction) }
|
|
@@ -47,7 +47,7 @@ module SolidStackWeb
|
|
|
47
47
|
count = SolidQueue::Job.where(id: job_ids).destroy_all.size
|
|
48
48
|
record_audit("jobs_discarded", item_count: count)
|
|
49
49
|
redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction),
|
|
50
|
-
notice: "
|
|
50
|
+
notice: t("solid_stack_web.flash.jobs_discarded", count: count)
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
@@ -5,14 +5,14 @@ module SolidStackWeb
|
|
|
5
5
|
result = task.enqueue(at: Time.current)
|
|
6
6
|
|
|
7
7
|
if result
|
|
8
|
-
redirect_to recurring_tasks_path, notice: "
|
|
8
|
+
redirect_to recurring_tasks_path, notice: t("solid_stack_web.flash.task_queued", key: task.key)
|
|
9
9
|
else
|
|
10
|
-
redirect_to recurring_tasks_path, alert: "
|
|
10
|
+
redirect_to recurring_tasks_path, alert: t("solid_stack_web.flash.cannot_enqueue_task", key: task.key)
|
|
11
11
|
end
|
|
12
12
|
rescue ActiveRecord::RecordNotFound
|
|
13
|
-
redirect_to recurring_tasks_path, alert: "
|
|
13
|
+
redirect_to recurring_tasks_path, alert: t("solid_stack_web.flash.task_not_found")
|
|
14
14
|
rescue => e
|
|
15
|
-
redirect_to recurring_tasks_path, alert: "
|
|
15
|
+
redirect_to recurring_tasks_path, alert: t("solid_stack_web.flash.cannot_run_task", error: e.message)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -6,10 +6,10 @@ module SolidStackWeb
|
|
|
6
6
|
SolidQueue::ScheduledExecution.where(job_id: job_ids).update_all(scheduled_at: 1.second.ago)
|
|
7
7
|
SolidQueue::Job.where(id: job_ids).update_all(scheduled_at: 1.second.ago)
|
|
8
8
|
redirect_to jobs_path(status: "scheduled", period: @period),
|
|
9
|
-
notice: "
|
|
9
|
+
notice: t("solid_stack_web.flash.jobs_run_immediately", count: job_ids.size)
|
|
10
10
|
rescue => e
|
|
11
11
|
redirect_to jobs_path(status: "scheduled", period: @period),
|
|
12
|
-
alert: "
|
|
12
|
+
alert: t("solid_stack_web.flash.cannot_run_jobs", error: e.message)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def update
|
|
@@ -24,14 +24,14 @@ module SolidStackWeb
|
|
|
24
24
|
respond_to do |format|
|
|
25
25
|
format.turbo_stream
|
|
26
26
|
format.html do
|
|
27
|
-
notice = @run_now ? "
|
|
27
|
+
notice = @run_now ? t("solid_stack_web.flash.job_run_immediately") : t("solid_stack_web.flash.job_rescheduled", offset: params[:offset])
|
|
28
28
|
redirect_to jobs_path(status: "scheduled", period: @period), notice: notice
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
rescue ArgumentError => e
|
|
32
32
|
redirect_to jobs_path(status: "scheduled"), alert: e.message
|
|
33
33
|
rescue => e
|
|
34
|
-
redirect_to jobs_path(status: "scheduled"), alert: "
|
|
34
|
+
redirect_to jobs_path(status: "scheduled"), alert: t("solid_stack_web.flash.cannot_reschedule_job", error: e.message)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
private
|
|
@@ -44,7 +44,7 @@ module SolidStackWeb
|
|
|
44
44
|
|
|
45
45
|
def resolve_new_time(execution, offset)
|
|
46
46
|
return 1.second.ago if offset == "now"
|
|
47
|
-
raise ArgumentError, "
|
|
47
|
+
raise ArgumentError, t("solid_stack_web.flash.invalid_offset") unless PERIOD_DURATIONS.key?(offset)
|
|
48
48
|
|
|
49
49
|
execution.scheduled_at + PERIOD_DURATIONS[offset]
|
|
50
50
|
end
|
|
@@ -35,11 +35,14 @@ module SolidStackWeb
|
|
|
35
35
|
build_sparkline_svg(
|
|
36
36
|
Struct.new(:buckets, :max).new(timeline.entry_buckets, timeline.entry_max),
|
|
37
37
|
css_class: "sqw-sparkline sqw-sparkline--lg",
|
|
38
|
-
aria_label: "
|
|
38
|
+
aria_label: t("solid_stack_web.helpers.cache_entry_this_hour", count: 0).gsub(/\d+/, "…")
|
|
39
39
|
) do |count, i|
|
|
40
40
|
hours_ago = CacheTimeline::HOURS - 1 - i
|
|
41
|
-
hours_ago.zero?
|
|
42
|
-
|
|
41
|
+
if hours_ago.zero?
|
|
42
|
+
t("solid_stack_web.helpers.cache_entry_this_hour", count: count)
|
|
43
|
+
else
|
|
44
|
+
t("solid_stack_web.helpers.cache_entry_hours_ago", count: count, hours: hours_ago)
|
|
45
|
+
end
|
|
43
46
|
end
|
|
44
47
|
end
|
|
45
48
|
|
|
@@ -47,11 +50,15 @@ module SolidStackWeb
|
|
|
47
50
|
build_sparkline_svg(
|
|
48
51
|
Struct.new(:buckets, :max).new(timeline.byte_buckets, timeline.byte_max),
|
|
49
52
|
css_class: "sqw-sparkline sqw-sparkline--lg",
|
|
50
|
-
aria_label: "
|
|
53
|
+
aria_label: t("solid_stack_web.cache.bytes_written")
|
|
51
54
|
) do |bytes, i|
|
|
52
55
|
hours_ago = CacheTimeline::HOURS - 1 - i
|
|
53
56
|
size = number_to_human_size(bytes)
|
|
54
|
-
hours_ago.zero?
|
|
57
|
+
if hours_ago.zero?
|
|
58
|
+
t("solid_stack_web.helpers.cache_size_this_hour", size: size)
|
|
59
|
+
else
|
|
60
|
+
t("solid_stack_web.helpers.cache_size_hours_ago", size: size, hours: hours_ago)
|
|
61
|
+
end
|
|
55
62
|
end
|
|
56
63
|
end
|
|
57
64
|
|
|
@@ -59,31 +66,37 @@ module SolidStackWeb
|
|
|
59
66
|
build_sparkline_svg(
|
|
60
67
|
Struct.new(:buckets, :max).new(timeline.message_buckets, timeline.message_max),
|
|
61
68
|
css_class: "sqw-sparkline sqw-sparkline--lg",
|
|
62
|
-
aria_label: "
|
|
69
|
+
aria_label: t("solid_stack_web.cable.messages_timeline")
|
|
63
70
|
) do |count, i|
|
|
64
71
|
hours_ago = CableTimeline::HOURS - 1 - i
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
if hours_ago.zero?
|
|
73
|
+
t("solid_stack_web.helpers.cable_message_this_hour", count: count)
|
|
74
|
+
else
|
|
75
|
+
t("solid_stack_web.helpers.cable_message_hours_ago", count: count, hours: hours_ago)
|
|
76
|
+
end
|
|
67
77
|
end
|
|
68
78
|
end
|
|
69
79
|
|
|
70
80
|
def throughput_sparkline_svg(sparkline)
|
|
71
|
-
build_sparkline_svg(sparkline, aria_label: "
|
|
81
|
+
build_sparkline_svg(sparkline, aria_label: t("solid_stack_web.dashboard.throughput_label")) do |count, i|
|
|
72
82
|
hours_ago = SolidStackWeb::ThroughputSparkline::HOURS - i
|
|
73
83
|
if hours_ago == 1
|
|
74
|
-
"
|
|
84
|
+
t("solid_stack_web.helpers.throughput_last_hour", count: count)
|
|
75
85
|
else
|
|
76
|
-
"
|
|
86
|
+
t("solid_stack_web.helpers.throughput_hours_ago", count: count, from: hours_ago, to: hours_ago - 1)
|
|
77
87
|
end
|
|
78
88
|
end
|
|
79
89
|
end
|
|
80
90
|
|
|
81
91
|
def queue_depth_sparkline_svg(sparkline)
|
|
82
92
|
build_sparkline_svg(sparkline, css_class: "sqw-sparkline sqw-sparkline--sm",
|
|
83
|
-
aria_label: "
|
|
93
|
+
aria_label: t("solid_stack_web.queues.col_depth")) do |count, i|
|
|
84
94
|
hours_ago = SolidStackWeb::QueueDepthSparkline::HOURS - 1 - i
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
if hours_ago.zero?
|
|
96
|
+
t("solid_stack_web.helpers.queue_depth_now", count: count)
|
|
97
|
+
else
|
|
98
|
+
t("solid_stack_web.helpers.queue_depth_hours_ago", count: count, hours: hours_ago)
|
|
99
|
+
end
|
|
87
100
|
end
|
|
88
101
|
end
|
|
89
102
|
|
|
@@ -110,12 +123,12 @@ module SolidStackWeb
|
|
|
110
123
|
end
|
|
111
124
|
|
|
112
125
|
def failed_job_sparkline_svg(sparkline)
|
|
113
|
-
build_sparkline_svg(sparkline, aria_label: "
|
|
126
|
+
build_sparkline_svg(sparkline, aria_label: t("solid_stack_web.dashboard.failures_label")) do |count, i|
|
|
114
127
|
hours_ago = SolidStackWeb::FailedJobSparkline::HOURS - i
|
|
115
128
|
if hours_ago == 1
|
|
116
|
-
"
|
|
129
|
+
t("solid_stack_web.helpers.failure_last_hour", count: count)
|
|
117
130
|
else
|
|
118
|
-
"
|
|
131
|
+
t("solid_stack_web.helpers.failure_hours_ago", count: count, from: hours_ago, to: hours_ago - 1)
|
|
119
132
|
end
|
|
120
133
|
end
|
|
121
134
|
end
|