solid_web_ui 0.1.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 +7 -0
- data/README.md +74 -0
- data/app/assets/stylesheets/solid_web_ui.css +232 -0
- data/app/components/solid_web_ui/ui/page_component.html.erb +15 -0
- data/app/components/solid_web_ui/ui/page_component.rb +22 -0
- data/app/components/solid_web_ui/ui/paginator_component.html.erb +21 -0
- data/app/components/solid_web_ui/ui/paginator_component.rb +23 -0
- data/app/components/solid_web_ui/ui/stat_card_component.html.erb +11 -0
- data/app/components/solid_web_ui/ui/stat_card_component.rb +24 -0
- data/app/components/solid_web_ui/ui/status_badge_component.rb +37 -0
- data/app/components/solid_web_ui/ui/table_component.html.erb +18 -0
- data/app/components/solid_web_ui/ui/table_component.rb +19 -0
- data/app/controllers/solid_web_ui/cable/application_controller.rb +14 -0
- data/app/controllers/solid_web_ui/cable/channels_controller.rb +13 -0
- data/app/controllers/solid_web_ui/cable/dashboard_controller.rb +13 -0
- data/app/controllers/solid_web_ui/cable/messages_controller.rb +18 -0
- data/app/controllers/solid_web_ui/cache/application_controller.rb +14 -0
- data/app/controllers/solid_web_ui/cache/dashboard_controller.rb +13 -0
- data/app/controllers/solid_web_ui/cache/entries_controller.rb +24 -0
- data/app/controllers/solid_web_ui/queue/application_controller.rb +17 -0
- data/app/controllers/solid_web_ui/queue/dashboard_controller.rb +18 -0
- data/app/controllers/solid_web_ui/queue/failed_executions_controller.rb +34 -0
- data/app/controllers/solid_web_ui/queue/jobs_controller.rb +24 -0
- data/app/controllers/solid_web_ui/queue/processes_controller.rb +9 -0
- data/app/controllers/solid_web_ui/queue/queues_controller.rb +27 -0
- data/app/controllers/solid_web_ui/queue/recurring_tasks_controller.rb +9 -0
- data/app/helpers/solid_web_ui/cable/application_helper.rb +22 -0
- data/app/helpers/solid_web_ui/cache/application_helper.rb +23 -0
- data/app/helpers/solid_web_ui/component_helper.rb +28 -0
- data/app/helpers/solid_web_ui/queue/application_helper.rb +38 -0
- data/app/views/layouts/solid_web_ui.html.erb +33 -0
- data/app/views/solid_web_ui/cable/channels/index.html.erb +12 -0
- data/app/views/solid_web_ui/cable/dashboard/index.html.erb +24 -0
- data/app/views/solid_web_ui/cache/dashboard/index.html.erb +15 -0
- data/app/views/solid_web_ui/cache/entries/index.html.erb +15 -0
- data/app/views/solid_web_ui/queue/dashboard/index.html.erb +13 -0
- data/app/views/solid_web_ui/queue/failed_executions/index.html.erb +26 -0
- data/app/views/solid_web_ui/queue/jobs/index.html.erb +23 -0
- data/app/views/solid_web_ui/queue/processes/index.html.erb +14 -0
- data/app/views/solid_web_ui/queue/queues/index.html.erb +25 -0
- data/app/views/solid_web_ui/queue/recurring_tasks/index.html.erb +17 -0
- data/lib/solid_web_ui/cable/engine.rb +13 -0
- data/lib/solid_web_ui/cable/routes.rb +7 -0
- data/lib/solid_web_ui/cable.rb +18 -0
- data/lib/solid_web_ui/cache/engine.rb +13 -0
- data/lib/solid_web_ui/cache/routes.rb +7 -0
- data/lib/solid_web_ui/cache.rb +17 -0
- data/lib/solid_web_ui/configurable.rb +20 -0
- data/lib/solid_web_ui/engine.rb +13 -0
- data/lib/solid_web_ui/paginator.rb +49 -0
- data/lib/solid_web_ui/queue/engine.rb +15 -0
- data/lib/solid_web_ui/queue/routes.rb +18 -0
- data/lib/solid_web_ui/queue.rb +19 -0
- data/lib/solid_web_ui/theme.rb +71 -0
- data/lib/solid_web_ui/version.rb +5 -0
- data/lib/solid_web_ui.rb +33 -0
- metadata +193 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 733e4f6cdd63c6b22b5e3ca4166576ebfe2db2a482d6b2fa7e25035e0a12c7a9
|
|
4
|
+
data.tar.gz: e1b1e0755169b3ba6f74d576f155bcce21bf029f5d1a2de3e833be7713d806e5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 66d72bca337446f1df7ae48b7faebe2c760c56e1079eba0a280deb1047bd6ef0a0719bcd64137609a0df7207bc7632239eff6920aa30f16fb28775f7d4b97fa9
|
|
7
|
+
data.tar.gz: ae241b3d25a1108b038b88a92e81e36da9137c5c3af126a1abd4c6767ca135e46b9f3200641239cc162a74c690bb73eaa3ad6145450a6b61231e60717afb5348
|
data/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# solid_web_ui
|
|
2
|
+
|
|
3
|
+
Web dashboards for Rails' **Solid Queue**, **Solid Cache** and **Solid Cable** — one gem
|
|
4
|
+
(`solid_web_ui`) with three independently mountable Rails engines sharing one design system.
|
|
5
|
+
|
|
6
|
+
> The repository is named `solid-web`; the gem it ships is `solid_web_ui`.
|
|
7
|
+
|
|
8
|
+
| Engine | Mount | What it does |
|
|
9
|
+
|--------|-------|--------------|
|
|
10
|
+
| `SolidWebUi::Queue::Engine` | `/admin/queue` | Solid Queue: dashboard, queues (pause/resume), jobs by status, failed jobs (retry/discard), processes, recurring tasks. |
|
|
11
|
+
| `SolidWebUi::Cache::Engine` | `/admin/cache` | Solid Cache: entry/size statistics, entry browser, clear. |
|
|
12
|
+
| `SolidWebUi::Cable::Engine` | `/admin/cable` | Solid Cable: message/channel activity, volume, retention trim. |
|
|
13
|
+
|
|
14
|
+
The shared core (`SolidWebUi`) provides the layout, ViewComponents, design-token theming and a
|
|
15
|
+
dry-configurable base. The engines are plain Rails mountable engines — **no ActiveAdmin required**;
|
|
16
|
+
host authentication is inherited through a configurable `base_controller_class`.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# Gemfile
|
|
22
|
+
gem "solid_web_ui"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# config/routes.rb — mount only the parts you want
|
|
27
|
+
mount SolidWebUi::Queue::Engine => "/admin/queue"
|
|
28
|
+
mount SolidWebUi::Cache::Engine => "/admin/cache"
|
|
29
|
+
mount SolidWebUi::Cable::Engine => "/admin/cable"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# config/initializers/solid_web_ui.rb — protect the dashboards behind your auth
|
|
34
|
+
SolidWebUi::Queue.config.base_controller_class = "Admin::BaseController"
|
|
35
|
+
SolidWebUi::Cache.config.base_controller_class = "Admin::BaseController"
|
|
36
|
+
SolidWebUi::Cable.config.base_controller_class = "Admin::BaseController"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
A single dummy Rails app under `spec/dummy` (SQLite, schema loaded from the Solid* gems) exercises
|
|
42
|
+
all three engines.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bundle install
|
|
46
|
+
bundle exec rspec # full suite
|
|
47
|
+
bundle exec rake assets:build # rebuild the precompiled Tailwind stylesheet
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> Requires the Ruby pinned for this workspace. In a non-interactive shell use mise:
|
|
51
|
+
> `mise exec -- bundle exec rspec`.
|
|
52
|
+
|
|
53
|
+
## Releasing
|
|
54
|
+
|
|
55
|
+
Pushing a version tag builds and releases the gem via
|
|
56
|
+
[`.github/workflows/release.yml`](.github/workflows/release.yml):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git tag v0.1.0
|
|
60
|
+
git push origin v0.1.0
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The workflow builds `solid_web_ui.gem`, attaches it to a GitHub Release, and publishes it to RubyGems
|
|
64
|
+
via **OIDC Trusted Publishing** (no API key / secret — MFA-compatible).
|
|
65
|
+
|
|
66
|
+
One-time setup on [rubygems.org](https://rubygems.org): register a trusted publisher for `solid_web_ui`
|
|
67
|
+
(repo `doromones/solid-web`, workflow `release.yml`). Before the first release use a *pending* trusted
|
|
68
|
+
publisher (`https://rubygems.org/profile/oidc/pending_trusted_publishers/new`).
|
|
69
|
+
|
|
70
|
+
## Documentation
|
|
71
|
+
|
|
72
|
+
- [Configuration reference](docs/configuration.md)
|
|
73
|
+
- [Theming — projecting the host design](docs/theming.md)
|
|
74
|
+
- [Authentication](docs/authentication.md)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer theme, components, utilities;
|
|
3
|
+
@layer utilities {
|
|
4
|
+
.static {
|
|
5
|
+
position: static;
|
|
6
|
+
}
|
|
7
|
+
.block {
|
|
8
|
+
display: block;
|
|
9
|
+
}
|
|
10
|
+
.contents {
|
|
11
|
+
display: contents;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
@layer components {
|
|
15
|
+
.solid-web-ui {
|
|
16
|
+
color: var(--swui-color-text);
|
|
17
|
+
background: var(--swui-color-bg);
|
|
18
|
+
font-family: var(--swui-font);
|
|
19
|
+
line-height: 1.5;
|
|
20
|
+
-webkit-font-smoothing: antialiased;
|
|
21
|
+
}
|
|
22
|
+
.solid-web-ui *, .solid-web-ui *::before, .solid-web-ui *::after {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
.solid-web-ui .swui-page {
|
|
26
|
+
max-width: 72rem;
|
|
27
|
+
margin-inline: auto;
|
|
28
|
+
padding: 1.5rem;
|
|
29
|
+
}
|
|
30
|
+
.solid-web-ui .swui-page__header {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-wrap: wrap;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: space-between;
|
|
35
|
+
gap: 1rem;
|
|
36
|
+
margin-bottom: 1.5rem;
|
|
37
|
+
border-bottom: 1px solid var(--swui-color-border);
|
|
38
|
+
padding-bottom: 1rem;
|
|
39
|
+
}
|
|
40
|
+
.solid-web-ui .swui-page__title {
|
|
41
|
+
font-size: 1.5rem;
|
|
42
|
+
font-weight: 700;
|
|
43
|
+
margin: 0;
|
|
44
|
+
}
|
|
45
|
+
.solid-web-ui .swui-nav {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-wrap: wrap;
|
|
48
|
+
gap: 0.25rem;
|
|
49
|
+
}
|
|
50
|
+
.solid-web-ui .swui-nav__link {
|
|
51
|
+
display: inline-block;
|
|
52
|
+
padding: 0.4rem 0.75rem;
|
|
53
|
+
border-radius: var(--swui-radius);
|
|
54
|
+
color: var(--swui-color-muted);
|
|
55
|
+
text-decoration: none;
|
|
56
|
+
font-size: 0.9rem;
|
|
57
|
+
font-weight: 500;
|
|
58
|
+
}
|
|
59
|
+
.solid-web-ui .swui-nav__link:hover {
|
|
60
|
+
background: var(--swui-color-primary);
|
|
61
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
62
|
+
background: color-mix(in srgb, var(--swui-color-primary) 10%, transparent);
|
|
63
|
+
}
|
|
64
|
+
color: var(--swui-color-text);
|
|
65
|
+
}
|
|
66
|
+
.solid-web-ui .swui-nav__link--active {
|
|
67
|
+
background: var(--swui-color-primary);
|
|
68
|
+
color: var(--swui-color-primary-contrast);
|
|
69
|
+
}
|
|
70
|
+
.solid-web-ui .swui-grid {
|
|
71
|
+
display: grid;
|
|
72
|
+
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
|
73
|
+
gap: 1rem;
|
|
74
|
+
margin-bottom: 1.5rem;
|
|
75
|
+
}
|
|
76
|
+
.solid-web-ui .swui-card {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
gap: 0.35rem;
|
|
80
|
+
padding: 1rem 1.1rem;
|
|
81
|
+
background: var(--swui-color-surface);
|
|
82
|
+
border: 1px solid var(--swui-color-border);
|
|
83
|
+
border-left: 3px solid var(--swui-color-border);
|
|
84
|
+
border-radius: var(--swui-radius);
|
|
85
|
+
text-decoration: none;
|
|
86
|
+
color: inherit;
|
|
87
|
+
}
|
|
88
|
+
.solid-web-ui a.swui-card:hover {
|
|
89
|
+
border-color: var(--swui-color-primary);
|
|
90
|
+
}
|
|
91
|
+
.solid-web-ui .swui-card__label {
|
|
92
|
+
font-size: 0.8rem;
|
|
93
|
+
color: var(--swui-color-muted);
|
|
94
|
+
text-transform: uppercase;
|
|
95
|
+
letter-spacing: 0.03em;
|
|
96
|
+
}
|
|
97
|
+
.solid-web-ui .swui-card__value {
|
|
98
|
+
font-size: 1.75rem;
|
|
99
|
+
font-weight: 700;
|
|
100
|
+
}
|
|
101
|
+
.solid-web-ui .swui-card--primary {
|
|
102
|
+
border-left-color: var(--swui-color-primary);
|
|
103
|
+
}
|
|
104
|
+
.solid-web-ui .swui-card--success {
|
|
105
|
+
border-left-color: var(--swui-color-success);
|
|
106
|
+
}
|
|
107
|
+
.solid-web-ui .swui-card--warning {
|
|
108
|
+
border-left-color: var(--swui-color-warning);
|
|
109
|
+
}
|
|
110
|
+
.solid-web-ui .swui-card--danger {
|
|
111
|
+
border-left-color: var(--swui-color-danger);
|
|
112
|
+
}
|
|
113
|
+
.solid-web-ui .swui-badge {
|
|
114
|
+
display: inline-block;
|
|
115
|
+
padding: 0.15rem 0.5rem;
|
|
116
|
+
border-radius: 999px;
|
|
117
|
+
font-size: 0.75rem;
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
background: var(--swui-color-muted);
|
|
120
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
121
|
+
background: color-mix(in srgb, var(--swui-color-muted) 18%, transparent);
|
|
122
|
+
}
|
|
123
|
+
color: var(--swui-color-text);
|
|
124
|
+
}
|
|
125
|
+
.solid-web-ui .swui-badge--info {
|
|
126
|
+
background: var(--swui-color-primary);
|
|
127
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
128
|
+
background: color-mix(in srgb, var(--swui-color-primary) 18%, transparent);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
.solid-web-ui .swui-badge--success {
|
|
132
|
+
background: var(--swui-color-success);
|
|
133
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
134
|
+
background: color-mix(in srgb, var(--swui-color-success) 22%, transparent);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
.solid-web-ui .swui-badge--warning {
|
|
138
|
+
background: var(--swui-color-warning);
|
|
139
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
140
|
+
background: color-mix(in srgb, var(--swui-color-warning) 24%, transparent);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
.solid-web-ui .swui-badge--danger {
|
|
144
|
+
background: var(--swui-color-danger);
|
|
145
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
146
|
+
background: color-mix(in srgb, var(--swui-color-danger) 22%, transparent);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
.solid-web-ui .swui-table {
|
|
150
|
+
width: 100%;
|
|
151
|
+
border-collapse: collapse;
|
|
152
|
+
background: var(--swui-color-surface);
|
|
153
|
+
border: 1px solid var(--swui-color-border);
|
|
154
|
+
border-radius: var(--swui-radius);
|
|
155
|
+
overflow: hidden;
|
|
156
|
+
font-size: 0.9rem;
|
|
157
|
+
}
|
|
158
|
+
.solid-web-ui .swui-table th, .solid-web-ui .swui-table td {
|
|
159
|
+
text-align: left;
|
|
160
|
+
padding: 0.6rem 0.85rem;
|
|
161
|
+
border-bottom: 1px solid var(--swui-color-border);
|
|
162
|
+
}
|
|
163
|
+
.solid-web-ui .swui-table th {
|
|
164
|
+
font-size: 0.75rem;
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
letter-spacing: 0.03em;
|
|
167
|
+
color: var(--swui-color-muted);
|
|
168
|
+
background: var(--swui-color-muted);
|
|
169
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
170
|
+
background: color-mix(in srgb, var(--swui-color-muted) 6%, transparent);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
.solid-web-ui .swui-table tr:last-child td {
|
|
174
|
+
border-bottom: 0;
|
|
175
|
+
}
|
|
176
|
+
.solid-web-ui .swui-table__empty td {
|
|
177
|
+
text-align: center;
|
|
178
|
+
color: var(--swui-color-muted);
|
|
179
|
+
padding: 1.5rem;
|
|
180
|
+
}
|
|
181
|
+
.solid-web-ui .swui-btn {
|
|
182
|
+
display: inline-block;
|
|
183
|
+
padding: 0.35rem 0.7rem;
|
|
184
|
+
border: 1px solid var(--swui-color-border);
|
|
185
|
+
border-radius: var(--swui-radius);
|
|
186
|
+
background: var(--swui-color-surface);
|
|
187
|
+
color: var(--swui-color-text);
|
|
188
|
+
font-size: 0.85rem;
|
|
189
|
+
font-weight: 500;
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
text-decoration: none;
|
|
192
|
+
}
|
|
193
|
+
.solid-web-ui .swui-btn:hover {
|
|
194
|
+
border-color: var(--swui-color-primary);
|
|
195
|
+
}
|
|
196
|
+
.solid-web-ui .swui-btn--danger {
|
|
197
|
+
color: var(--swui-color-danger);
|
|
198
|
+
border-color: var(--swui-color-danger);
|
|
199
|
+
}
|
|
200
|
+
.solid-web-ui .swui-pagination {
|
|
201
|
+
display: flex;
|
|
202
|
+
flex-wrap: wrap;
|
|
203
|
+
gap: 0.25rem;
|
|
204
|
+
margin-top: 1rem;
|
|
205
|
+
}
|
|
206
|
+
.solid-web-ui .swui-pagination__item {
|
|
207
|
+
display: inline-block;
|
|
208
|
+
min-width: 2rem;
|
|
209
|
+
text-align: center;
|
|
210
|
+
padding: 0.3rem 0.55rem;
|
|
211
|
+
border: 1px solid var(--swui-color-border);
|
|
212
|
+
border-radius: var(--swui-radius);
|
|
213
|
+
color: var(--swui-color-text);
|
|
214
|
+
text-decoration: none;
|
|
215
|
+
font-size: 0.85rem;
|
|
216
|
+
}
|
|
217
|
+
.solid-web-ui .swui-pagination__item--current {
|
|
218
|
+
background: var(--swui-color-primary);
|
|
219
|
+
color: var(--swui-color-primary-contrast);
|
|
220
|
+
border-color: var(--swui-color-primary);
|
|
221
|
+
}
|
|
222
|
+
.solid-web-ui .swui-pagination__item--disabled {
|
|
223
|
+
color: var(--swui-color-muted);
|
|
224
|
+
opacity: 0.5;
|
|
225
|
+
pointer-events: none;
|
|
226
|
+
}
|
|
227
|
+
.solid-web-ui .swui-section-title {
|
|
228
|
+
font-size: 1.05rem;
|
|
229
|
+
font-weight: 600;
|
|
230
|
+
margin: 1.5rem 0 0.75rem;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<div class="swui-page">
|
|
2
|
+
<header class="swui-page__header">
|
|
3
|
+
<h1 class="swui-page__title"><%= title %></h1>
|
|
4
|
+
<% if nav.any? %>
|
|
5
|
+
<nav class="swui-nav">
|
|
6
|
+
<% nav.each do |item| %>
|
|
7
|
+
<%= link_to item[:label], item[:href], class: nav_link_class(item) %>
|
|
8
|
+
<% end %>
|
|
9
|
+
</nav>
|
|
10
|
+
<% end %>
|
|
11
|
+
</header>
|
|
12
|
+
<main class="swui-page__body">
|
|
13
|
+
<%= content %>
|
|
14
|
+
</main>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi
|
|
4
|
+
module Ui
|
|
5
|
+
# Page chrome shared by every dashboard screen: a title, an optional nav bar
|
|
6
|
+
# (array of { label:, href:, active: }) and the page body as content.
|
|
7
|
+
class PageComponent < ViewComponent::Base
|
|
8
|
+
def initialize(title:, nav: [])
|
|
9
|
+
@title = title
|
|
10
|
+
@nav = nav || []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
attr_reader :title, :nav
|
|
16
|
+
|
|
17
|
+
def nav_link_class(item)
|
|
18
|
+
[ "swui-nav__link", item[:active] ? "swui-nav__link--active" : nil ].compact.join(" ")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<nav class="swui-pagination" aria-label="Pagination">
|
|
2
|
+
<% if paginator.prev_page %>
|
|
3
|
+
<%= link_to "‹ Prev", page_url.call(paginator.prev_page), class: "swui-pagination__item" %>
|
|
4
|
+
<% else %>
|
|
5
|
+
<span class="swui-pagination__item swui-pagination__item--disabled">‹ Prev</span>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<% (1..paginator.total_pages).each do |number| %>
|
|
9
|
+
<% if number == paginator.current_page %>
|
|
10
|
+
<span class="swui-pagination__item swui-pagination__item--current" aria-current="page"><%= number %></span>
|
|
11
|
+
<% else %>
|
|
12
|
+
<%= link_to number, page_url.call(number), class: "swui-pagination__item" %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<% if paginator.next_page %>
|
|
17
|
+
<%= link_to "Next ›", page_url.call(paginator.next_page), class: "swui-pagination__item" %>
|
|
18
|
+
<% else %>
|
|
19
|
+
<span class="swui-pagination__item swui-pagination__item--disabled">Next ›</span>
|
|
20
|
+
<% end %>
|
|
21
|
+
</nav>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi
|
|
4
|
+
module Ui
|
|
5
|
+
# Renders pagination controls for a SolidWebUi::Paginator. `page_url` is a
|
|
6
|
+
# callable mapping a page number to a URL (so the component stays decoupled
|
|
7
|
+
# from any engine's routes). Hidden entirely when there is only one page.
|
|
8
|
+
class PaginatorComponent < ViewComponent::Base
|
|
9
|
+
def initialize(paginator:, page_url:)
|
|
10
|
+
@paginator = paginator
|
|
11
|
+
@page_url = page_url
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render?
|
|
15
|
+
@paginator.total_pages > 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :paginator, :page_url
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<% if href %>
|
|
2
|
+
<%= link_to href, class: css_classes do %>
|
|
3
|
+
<span class="swui-card__label"><%= label %></span>
|
|
4
|
+
<span class="swui-card__value"><%= value %></span>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% else %>
|
|
7
|
+
<div class="<%= css_classes %>">
|
|
8
|
+
<span class="swui-card__label"><%= label %></span>
|
|
9
|
+
<span class="swui-card__value"><%= value %></span>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi
|
|
4
|
+
module Ui
|
|
5
|
+
# A single dashboard metric: a label and a (usually numeric) value, optionally
|
|
6
|
+
# toned (neutral/primary/success/warning/danger) and linkable.
|
|
7
|
+
class StatCardComponent < ViewComponent::Base
|
|
8
|
+
def initialize(label:, value:, tone: :neutral, href: nil)
|
|
9
|
+
@label = label
|
|
10
|
+
@value = value
|
|
11
|
+
@tone = tone
|
|
12
|
+
@href = href
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :label, :value, :href
|
|
18
|
+
|
|
19
|
+
def css_classes
|
|
20
|
+
[ "swui-card", "swui-card--#{@tone}" ].join(" ")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi
|
|
4
|
+
module Ui
|
|
5
|
+
# A small pill conveying a record's status. Maps a domain status symbol to one
|
|
6
|
+
# of the shared tone classes; unknown statuses degrade to a neutral pill.
|
|
7
|
+
class StatusBadgeComponent < ViewComponent::Base
|
|
8
|
+
TONES = {
|
|
9
|
+
ready: :info,
|
|
10
|
+
scheduled: :info,
|
|
11
|
+
in_progress: :info,
|
|
12
|
+
claimed: :info,
|
|
13
|
+
finished: :success,
|
|
14
|
+
succeeded: :success,
|
|
15
|
+
failed: :danger,
|
|
16
|
+
blocked: :warning,
|
|
17
|
+
paused: :warning,
|
|
18
|
+
stale: :warning
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def initialize(label:, status: nil)
|
|
22
|
+
@label = label
|
|
23
|
+
@status = status
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call
|
|
27
|
+
content_tag(:span, @label, class: "swui-badge swui-badge--#{tone}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def tone
|
|
33
|
+
TONES.fetch(@status&.to_sym, :neutral)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<table class="swui-table">
|
|
2
|
+
<thead>
|
|
3
|
+
<tr>
|
|
4
|
+
<% headers.each do |header| %>
|
|
5
|
+
<th scope="col"><%= header %></th>
|
|
6
|
+
<% end %>
|
|
7
|
+
</tr>
|
|
8
|
+
</thead>
|
|
9
|
+
<tbody>
|
|
10
|
+
<% if content.present? %>
|
|
11
|
+
<%= content %>
|
|
12
|
+
<% else %>
|
|
13
|
+
<tr class="swui-table__empty">
|
|
14
|
+
<td colspan="<%= headers.size %>"><%= empty_message %></td>
|
|
15
|
+
</tr>
|
|
16
|
+
<% end %>
|
|
17
|
+
</tbody>
|
|
18
|
+
</table>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi
|
|
4
|
+
module Ui
|
|
5
|
+
# A styled data table. Pass column `headers`; render the body rows as the
|
|
6
|
+
# component's content (a block of <tr> rows). Shows an empty state when no
|
|
7
|
+
# rows are given.
|
|
8
|
+
class TableComponent < ViewComponent::Base
|
|
9
|
+
def initialize(headers:, empty_message: "Nothing to show.")
|
|
10
|
+
@headers = headers
|
|
11
|
+
@empty_message = empty_message
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
attr_reader :headers, :empty_message
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi::Cable
|
|
4
|
+
class ApplicationController < SolidWebUi.resolve_base_controller(SolidWebUi::Cable.config.base_controller_class)
|
|
5
|
+
layout "solid_web_ui"
|
|
6
|
+
helper SolidWebUi::ComponentHelper
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def trimmable_scope
|
|
11
|
+
SolidCable::Message.where(created_at: ...SolidWebUi::Cable.config.retention.ago)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi::Cable
|
|
4
|
+
class ChannelsController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
counts = SolidCable::Message.group(:channel).count
|
|
7
|
+
last_seen = SolidCable::Message.group(:channel).maximum(:created_at)
|
|
8
|
+
@channels = counts
|
|
9
|
+
.map { |channel, count| { name: channel, count: count, last: last_seen[channel] } }
|
|
10
|
+
.sort_by { |row| -row[:count] }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi::Cable
|
|
4
|
+
class DashboardController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@total = SolidCable::Message.count
|
|
7
|
+
@channel_count = SolidCable::Message.distinct.count(:channel)
|
|
8
|
+
@trimmable = trimmable_scope.count
|
|
9
|
+
@last_hour = SolidCable::Message.where(created_at: 1.hour.ago..).count
|
|
10
|
+
@top_channels = SolidCable::Message.group(:channel).count.max_by(10) { |_channel, count| count }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi::Cable
|
|
4
|
+
class MessagesController < ApplicationController
|
|
5
|
+
before_action :ensure_trim_enabled, only: :trim
|
|
6
|
+
|
|
7
|
+
def trim
|
|
8
|
+
deleted = trimmable_scope.delete_all
|
|
9
|
+
redirect_to root_path, notice: "Trimmed #{deleted} old message(s)."
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def ensure_trim_enabled
|
|
15
|
+
head :forbidden unless SolidWebUi::Cable.config.enable_trim
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi::Cache
|
|
4
|
+
class ApplicationController < SolidWebUi.resolve_base_controller(SolidWebUi::Cache.config.base_controller_class)
|
|
5
|
+
layout "solid_web_ui"
|
|
6
|
+
helper SolidWebUi::ComponentHelper
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def per_page
|
|
11
|
+
SolidWebUi::Cache.config.per_page
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi::Cache
|
|
4
|
+
class DashboardController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@count = SolidCache::Entry.count
|
|
7
|
+
@total_bytes = SolidCache::Entry.sum(:byte_size)
|
|
8
|
+
@avg_bytes = SolidCache::Entry.average(:byte_size).to_f.round
|
|
9
|
+
@oldest = SolidCache::Entry.minimum(:created_at)
|
|
10
|
+
@newest = SolidCache::Entry.maximum(:created_at)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi::Cache
|
|
4
|
+
class EntriesController < ApplicationController
|
|
5
|
+
before_action :ensure_clear_enabled, only: :clear
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
scope = SolidCache::Entry.order(id: :desc)
|
|
9
|
+
@paginator = SolidWebUi::Paginator.new(scope, page: params[:page], per_page: per_page)
|
|
10
|
+
@entries = @paginator.records
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def clear
|
|
14
|
+
SolidCache::Entry.delete_all
|
|
15
|
+
redirect_to root_path, notice: "Cache cleared."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def ensure_clear_enabled
|
|
21
|
+
head :forbidden unless SolidWebUi::Cache.config.enable_clear
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi::Queue
|
|
4
|
+
# Inherits from the host-configured controller (default ActionController::Base)
|
|
5
|
+
# so host authentication/authorization applies. Resolved lazily at autoload
|
|
6
|
+
# time, after host initializers have set `base_controller_class`.
|
|
7
|
+
class ApplicationController < SolidWebUi.resolve_base_controller(SolidWebUi::Queue.config.base_controller_class)
|
|
8
|
+
layout "solid_web_ui"
|
|
9
|
+
helper SolidWebUi::ComponentHelper
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def per_page
|
|
14
|
+
SolidWebUi::Queue.config.per_page
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidWebUi::Queue
|
|
4
|
+
class DashboardController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@counts = {
|
|
7
|
+
ready: SolidQueue::ReadyExecution.count,
|
|
8
|
+
scheduled: SolidQueue::ScheduledExecution.count,
|
|
9
|
+
in_progress: SolidQueue::ClaimedExecution.count,
|
|
10
|
+
blocked: SolidQueue::BlockedExecution.count,
|
|
11
|
+
failed: SolidQueue::FailedExecution.count,
|
|
12
|
+
finished: SolidQueue::Job.where.not(finished_at: nil).count
|
|
13
|
+
}
|
|
14
|
+
@queue_count = SolidQueue::Queue.all.size
|
|
15
|
+
@process_count = SolidQueue::Process.count
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|