solid_web_ui 0.3.0 → 0.4.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/app/assets/javascripts/solid_web_ui.js +20 -0
- data/app/assets/stylesheets/solid_web_ui.css +194 -1
- data/app/controllers/solid_web_ui/cable/application_controller.rb +4 -0
- data/app/controllers/solid_web_ui/cable/channels_controller.rb +16 -2
- data/app/controllers/solid_web_ui/cable/messages_controller.rb +4 -0
- data/app/controllers/solid_web_ui/cache/entries_controller.rb +182 -1
- data/app/controllers/solid_web_ui/queue/jobs_controller.rb +7 -0
- data/app/helpers/solid_web_ui/cable/application_helper.rb +7 -2
- data/app/helpers/solid_web_ui/cache/application_helper.rb +29 -2
- data/app/helpers/solid_web_ui/queue/application_helper.rb +28 -0
- data/app/views/layouts/solid_web_ui.html.erb +12 -0
- data/app/views/solid_web_ui/cable/channels/index.html.erb +2 -2
- data/app/views/solid_web_ui/cable/channels/show.html.erb +35 -0
- data/app/views/solid_web_ui/cable/messages/show.html.erb +21 -0
- data/app/views/solid_web_ui/cache/entries/_form.html.erb +47 -0
- data/app/views/solid_web_ui/cache/entries/edit.html.erb +9 -0
- data/app/views/solid_web_ui/cache/entries/index.html.erb +17 -3
- data/app/views/solid_web_ui/cache/entries/new.html.erb +8 -0
- data/app/views/solid_web_ui/cache/entries/show.html.erb +50 -0
- data/app/views/solid_web_ui/queue/jobs/index.html.erb +2 -2
- data/app/views/solid_web_ui/queue/jobs/show.html.erb +62 -0
- data/lib/solid_web_ui/cable/routes.rb +2 -0
- data/lib/solid_web_ui/cache/routes.rb +8 -0
- data/lib/solid_web_ui/cache.rb +3 -0
- data/lib/solid_web_ui/queue/routes.rb +1 -0
- data/lib/solid_web_ui/theme.rb +4 -1
- data/lib/solid_web_ui/version.rb +1 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9036bf52798c82718cce437f3ba58c1ca43f5f325d4a185bc7fb942a1dadfed7
|
|
4
|
+
data.tar.gz: fc33a3c5f65913a9aa9f59e5d3e1578a8ec89ba38a76cea55b4e79b38c980bea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce886a4c5e21b2aa6fe722281338fb07619fb6302b9577cd2e736ec6381b14d797191715a54f25c149dfc31b212fda51fc5937c90f20c4df86240d0526446333
|
|
7
|
+
data.tar.gz: 855442dad92195d237732c456662472fe03b9daca0e07a72cff4f334913a0f0249582465f193adf5fa40ec650d0b6feb8688e99593443c5eb19ab755e23c6ccf
|
|
@@ -123,3 +123,23 @@
|
|
|
123
123
|
document.addEventListener("turbo:load", bindAll);
|
|
124
124
|
window.setInterval(tick, TICK_MS);
|
|
125
125
|
})();
|
|
126
|
+
|
|
127
|
+
// Clickable table rows: a row carrying data-swui-row-href navigates to that URL.
|
|
128
|
+
// A single delegated listener (bound once, survives frame morphs). Clicks on an
|
|
129
|
+
// inner link/button keep their own behaviour, and modified/middle clicks are left
|
|
130
|
+
// to the browser so "open in new tab" still works via the row's ID link.
|
|
131
|
+
(function () {
|
|
132
|
+
"use strict";
|
|
133
|
+
|
|
134
|
+
document.addEventListener("click", function (event) {
|
|
135
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey ||
|
|
136
|
+
event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
137
|
+
if (event.target.closest("a, button, input, label, select, textarea")) return;
|
|
138
|
+
|
|
139
|
+
var row = event.target.closest("tr[data-swui-row-href]");
|
|
140
|
+
if (!row) return;
|
|
141
|
+
|
|
142
|
+
var href = row.getAttribute("data-swui-row-href");
|
|
143
|
+
if (href) window.location.assign(href);
|
|
144
|
+
});
|
|
145
|
+
})();
|
|
@@ -2,9 +2,30 @@
|
|
|
2
2
|
@layer properties;
|
|
3
3
|
@layer theme, components, utilities;
|
|
4
4
|
@layer utilities {
|
|
5
|
+
.visible {
|
|
6
|
+
visibility: visible;
|
|
7
|
+
}
|
|
5
8
|
.static {
|
|
6
9
|
position: static;
|
|
7
10
|
}
|
|
11
|
+
.container {
|
|
12
|
+
width: 100%;
|
|
13
|
+
@media (width >= 40rem) {
|
|
14
|
+
max-width: 40rem;
|
|
15
|
+
}
|
|
16
|
+
@media (width >= 48rem) {
|
|
17
|
+
max-width: 48rem;
|
|
18
|
+
}
|
|
19
|
+
@media (width >= 64rem) {
|
|
20
|
+
max-width: 64rem;
|
|
21
|
+
}
|
|
22
|
+
@media (width >= 80rem) {
|
|
23
|
+
max-width: 80rem;
|
|
24
|
+
}
|
|
25
|
+
@media (width >= 96rem) {
|
|
26
|
+
max-width: 96rem;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
8
29
|
.block {
|
|
9
30
|
display: block;
|
|
10
31
|
}
|
|
@@ -14,6 +35,9 @@
|
|
|
14
35
|
.hidden {
|
|
15
36
|
display: none;
|
|
16
37
|
}
|
|
38
|
+
.table {
|
|
39
|
+
display: table;
|
|
40
|
+
}
|
|
17
41
|
.filter {
|
|
18
42
|
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
|
19
43
|
}
|
|
@@ -29,8 +53,17 @@
|
|
|
29
53
|
.solid-web-ui *, .solid-web-ui *::before, .solid-web-ui *::after {
|
|
30
54
|
box-sizing: border-box;
|
|
31
55
|
}
|
|
56
|
+
.solid-web-ui[data-color-scheme="light"] {
|
|
57
|
+
color-scheme: light;
|
|
58
|
+
}
|
|
59
|
+
.solid-web-ui[data-color-scheme="dark"] {
|
|
60
|
+
color-scheme: dark;
|
|
61
|
+
}
|
|
62
|
+
.solid-web-ui[data-color-scheme="auto"] {
|
|
63
|
+
color-scheme: light dark;
|
|
64
|
+
}
|
|
32
65
|
.solid-web-ui .swui-page {
|
|
33
|
-
max-width: 72rem;
|
|
66
|
+
max-width: var(--swui-page-max-width, 72rem);
|
|
34
67
|
margin-inline: auto;
|
|
35
68
|
padding: 1.5rem;
|
|
36
69
|
}
|
|
@@ -224,6 +257,15 @@
|
|
|
224
257
|
.solid-web-ui .swui-table tr:last-child td {
|
|
225
258
|
border-bottom: 0;
|
|
226
259
|
}
|
|
260
|
+
.solid-web-ui .swui-table tbody tr[data-swui-row-href] {
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
}
|
|
263
|
+
.solid-web-ui .swui-table tbody tr[data-swui-row-href]:hover {
|
|
264
|
+
background: var(--swui-color-primary);
|
|
265
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
266
|
+
background: color-mix(in srgb, var(--swui-color-primary) 8%, transparent);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
227
269
|
.solid-web-ui .swui-table__empty td {
|
|
228
270
|
text-align: center;
|
|
229
271
|
color: var(--swui-color-muted);
|
|
@@ -280,6 +322,157 @@
|
|
|
280
322
|
font-weight: 600;
|
|
281
323
|
margin: 1.5rem 0 0.75rem;
|
|
282
324
|
}
|
|
325
|
+
.solid-web-ui .swui-back {
|
|
326
|
+
display: inline-block;
|
|
327
|
+
margin-bottom: 1rem;
|
|
328
|
+
color: var(--swui-color-muted);
|
|
329
|
+
text-decoration: none;
|
|
330
|
+
font-size: 0.85rem;
|
|
331
|
+
}
|
|
332
|
+
.solid-web-ui .swui-back:hover {
|
|
333
|
+
color: var(--swui-color-text);
|
|
334
|
+
}
|
|
335
|
+
.solid-web-ui .swui-detail {
|
|
336
|
+
display: grid;
|
|
337
|
+
grid-template-columns: minmax(8rem, 14rem) 1fr;
|
|
338
|
+
gap: 0;
|
|
339
|
+
margin: 0 0 1.5rem;
|
|
340
|
+
background: var(--swui-color-surface);
|
|
341
|
+
border: 1px solid var(--swui-color-border);
|
|
342
|
+
border-radius: var(--swui-radius);
|
|
343
|
+
overflow: hidden;
|
|
344
|
+
font-size: 0.9rem;
|
|
345
|
+
}
|
|
346
|
+
.solid-web-ui .swui-detail dt, .solid-web-ui .swui-detail dd {
|
|
347
|
+
padding: 0.6rem 0.85rem;
|
|
348
|
+
border-bottom: 1px solid var(--swui-color-border);
|
|
349
|
+
margin: 0;
|
|
350
|
+
}
|
|
351
|
+
.solid-web-ui .swui-detail dt {
|
|
352
|
+
font-size: 0.75rem;
|
|
353
|
+
text-transform: uppercase;
|
|
354
|
+
letter-spacing: 0.03em;
|
|
355
|
+
color: var(--swui-color-muted);
|
|
356
|
+
background: var(--swui-color-muted);
|
|
357
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
358
|
+
background: color-mix(in srgb, var(--swui-color-muted) 6%, transparent);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
.solid-web-ui .swui-detail > dt:last-of-type, .solid-web-ui .swui-detail > dd:last-of-type {
|
|
362
|
+
border-bottom: 0;
|
|
363
|
+
}
|
|
364
|
+
.solid-web-ui .swui-detail__hint {
|
|
365
|
+
text-transform: none;
|
|
366
|
+
letter-spacing: 0;
|
|
367
|
+
opacity: 0.8;
|
|
368
|
+
}
|
|
369
|
+
.solid-web-ui .swui-flash-region {
|
|
370
|
+
max-width: var(--swui-page-max-width, 72rem);
|
|
371
|
+
margin-inline: auto;
|
|
372
|
+
padding: 1.5rem 1.5rem 0;
|
|
373
|
+
}
|
|
374
|
+
.solid-web-ui .swui-flash {
|
|
375
|
+
padding: 0.6rem 0.85rem;
|
|
376
|
+
margin-bottom: 1rem;
|
|
377
|
+
border: 1px solid var(--swui-color-border);
|
|
378
|
+
border-radius: var(--swui-radius);
|
|
379
|
+
font-size: 0.9rem;
|
|
380
|
+
}
|
|
381
|
+
.solid-web-ui .swui-flash--notice {
|
|
382
|
+
border-color: var(--swui-color-success);
|
|
383
|
+
background: var(--swui-color-success);
|
|
384
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
385
|
+
background: color-mix(in srgb, var(--swui-color-success) 14%, transparent);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
.solid-web-ui .swui-flash--alert {
|
|
389
|
+
border-color: var(--swui-color-danger);
|
|
390
|
+
background: var(--swui-color-danger);
|
|
391
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
392
|
+
background: color-mix(in srgb, var(--swui-color-danger) 14%, transparent);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
.solid-web-ui .swui-actions {
|
|
396
|
+
display: flex;
|
|
397
|
+
flex-wrap: wrap;
|
|
398
|
+
gap: 0.5rem;
|
|
399
|
+
margin-bottom: 1rem;
|
|
400
|
+
}
|
|
401
|
+
.solid-web-ui .swui-form {
|
|
402
|
+
max-width: 48rem;
|
|
403
|
+
margin-bottom: 1.5rem;
|
|
404
|
+
}
|
|
405
|
+
.solid-web-ui .swui-field {
|
|
406
|
+
margin-bottom: 1rem;
|
|
407
|
+
}
|
|
408
|
+
.solid-web-ui .swui-fieldset {
|
|
409
|
+
margin: 0 0 1rem;
|
|
410
|
+
padding: 1rem 1.1rem 0.25rem;
|
|
411
|
+
border: 1px solid var(--swui-color-border);
|
|
412
|
+
border-radius: var(--swui-radius);
|
|
413
|
+
}
|
|
414
|
+
.solid-web-ui .swui-fieldset legend {
|
|
415
|
+
padding: 0 0.4rem;
|
|
416
|
+
}
|
|
417
|
+
.solid-web-ui .swui-label {
|
|
418
|
+
display: block;
|
|
419
|
+
margin-bottom: 0.35rem;
|
|
420
|
+
font-size: 0.8rem;
|
|
421
|
+
font-weight: 600;
|
|
422
|
+
text-transform: uppercase;
|
|
423
|
+
letter-spacing: 0.03em;
|
|
424
|
+
color: var(--swui-color-muted);
|
|
425
|
+
}
|
|
426
|
+
.solid-web-ui .swui-input, .solid-web-ui .swui-textarea {
|
|
427
|
+
width: 100%;
|
|
428
|
+
padding: 0.5rem 0.65rem;
|
|
429
|
+
border: 1px solid var(--swui-color-border);
|
|
430
|
+
border-radius: var(--swui-radius);
|
|
431
|
+
background: var(--swui-color-surface);
|
|
432
|
+
color: var(--swui-color-text);
|
|
433
|
+
font: inherit;
|
|
434
|
+
font-size: 0.9rem;
|
|
435
|
+
}
|
|
436
|
+
.solid-web-ui .swui-textarea {
|
|
437
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
438
|
+
line-height: 1.45;
|
|
439
|
+
resize: vertical;
|
|
440
|
+
}
|
|
441
|
+
.solid-web-ui .swui-input:focus, .solid-web-ui .swui-textarea:focus {
|
|
442
|
+
outline: none;
|
|
443
|
+
border-color: var(--swui-color-primary);
|
|
444
|
+
}
|
|
445
|
+
.solid-web-ui .swui-input:disabled {
|
|
446
|
+
opacity: 0.6;
|
|
447
|
+
cursor: not-allowed;
|
|
448
|
+
}
|
|
449
|
+
.solid-web-ui .swui-hint {
|
|
450
|
+
margin: 0.35rem 0 0;
|
|
451
|
+
font-size: 0.8rem;
|
|
452
|
+
color: var(--swui-color-muted);
|
|
453
|
+
}
|
|
454
|
+
.solid-web-ui .swui-form__actions {
|
|
455
|
+
display: flex;
|
|
456
|
+
gap: 0.5rem;
|
|
457
|
+
margin-top: 1.25rem;
|
|
458
|
+
}
|
|
459
|
+
.solid-web-ui .swui-codeblock {
|
|
460
|
+
margin: 0;
|
|
461
|
+
padding: 0.75rem;
|
|
462
|
+
max-height: 24rem;
|
|
463
|
+
overflow: auto;
|
|
464
|
+
background: var(--swui-color-muted);
|
|
465
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
466
|
+
background: color-mix(in srgb, var(--swui-color-muted) 8%, transparent);
|
|
467
|
+
}
|
|
468
|
+
border: 1px solid var(--swui-color-border);
|
|
469
|
+
border-radius: var(--swui-radius);
|
|
470
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
471
|
+
font-size: 0.8rem;
|
|
472
|
+
line-height: 1.45;
|
|
473
|
+
white-space: pre;
|
|
474
|
+
color: var(--swui-color-text);
|
|
475
|
+
}
|
|
283
476
|
}
|
|
284
477
|
@property --tw-blur {
|
|
285
478
|
syntax: "*";
|
|
@@ -3,11 +3,25 @@
|
|
|
3
3
|
module SolidWebUi::Cable
|
|
4
4
|
class ChannelsController < ApplicationController
|
|
5
5
|
def index
|
|
6
|
-
counts = SolidCable::Message.group(:channel).count
|
|
6
|
+
counts = SolidCable::Message.group(:channel, :channel_hash).count
|
|
7
7
|
last_seen = SolidCable::Message.group(:channel).maximum(:created_at)
|
|
8
8
|
@channels = counts
|
|
9
|
-
.map { |channel, count| { name: channel, count: count, last: last_seen[channel] } }
|
|
9
|
+
.map { |(channel, hash), count| { name: channel, hash: hash, count: count, last: last_seen[channel] } }
|
|
10
10
|
.sort_by { |row| -row[:count] }
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
scope = SolidCable::Message.where(channel_hash: params[:channel_hash])
|
|
15
|
+
@name = scope.limit(1).pick(:channel)
|
|
16
|
+
raise ActiveRecord::RecordNotFound if @name.nil?
|
|
17
|
+
|
|
18
|
+
@channel_hash = params[:channel_hash]
|
|
19
|
+
@count = scope.count
|
|
20
|
+
@first = scope.minimum(:created_at)
|
|
21
|
+
@last = scope.maximum(:created_at)
|
|
22
|
+
|
|
23
|
+
@paginator = SolidWebUi::Paginator.new(scope.order(id: :desc), page: params[:page], per_page: per_page)
|
|
24
|
+
@messages = @paginator.records
|
|
25
|
+
end
|
|
12
26
|
end
|
|
13
27
|
end
|
|
@@ -4,6 +4,10 @@ module SolidWebUi::Cable
|
|
|
4
4
|
class MessagesController < ApplicationController
|
|
5
5
|
before_action :ensure_trim_enabled, only: :trim
|
|
6
6
|
|
|
7
|
+
def show
|
|
8
|
+
@message = SolidCable::Message.find(params[:id])
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
def trim
|
|
8
12
|
deleted = trimmable_scope.delete_all
|
|
9
13
|
redirect_to root_path, notice: "Trimmed #{deleted} old message(s)."
|
|
@@ -2,12 +2,94 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidWebUi::Cache
|
|
4
4
|
class EntriesController < ApplicationController
|
|
5
|
-
before_action :ensure_clear_enabled,
|
|
5
|
+
before_action :ensure_clear_enabled, only: :clear
|
|
6
|
+
before_action :ensure_create_enabled, only: %i[new create]
|
|
7
|
+
before_action :ensure_edit_enabled, only: %i[edit update]
|
|
8
|
+
before_action :ensure_delete_enabled, only: :destroy
|
|
9
|
+
before_action :set_entry, only: %i[show edit update destroy]
|
|
6
10
|
|
|
7
11
|
def index
|
|
8
12
|
scope = SolidCache::Entry.order(id: :desc)
|
|
9
13
|
@paginator = SolidWebUi::Paginator.new(scope, page: params[:page], per_page: per_page)
|
|
10
14
|
@entries = @paginator.records
|
|
15
|
+
# Per-entry expiry, read from each cache envelope's header (no value
|
|
16
|
+
# deserialization). Numeric epoch = has a TTL, :never = decodable but no
|
|
17
|
+
# TTL, :undecodable = couldn't be read.
|
|
18
|
+
@expiry = @entries.to_h { |entry| [ entry.id, entry_expiry(entry) ] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show
|
|
22
|
+
@decoded = decode_value(@entry.value)
|
|
23
|
+
value = resolved_value(@decoded)
|
|
24
|
+
@value_readable = value != UNREADABLE
|
|
25
|
+
@value_text = value.is_a?(String) ? value : value.inspect if @value_readable
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def new
|
|
29
|
+
@key = ""
|
|
30
|
+
@value = ""
|
|
31
|
+
@expires_at = ""
|
|
32
|
+
@version = ""
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create
|
|
36
|
+
@key = params[:key].to_s
|
|
37
|
+
@value = params[:value].to_s
|
|
38
|
+
@expires_at = params[:expires_at].to_s
|
|
39
|
+
@version = params[:version].to_s
|
|
40
|
+
|
|
41
|
+
if @key.empty?
|
|
42
|
+
return render_new_error("Key can't be blank.")
|
|
43
|
+
elsif SolidCache::Entry.read(@key)
|
|
44
|
+
return render_new_error("An entry with that key already exists.")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
expires_at = parse_expires_at(@expires_at)
|
|
48
|
+
SolidCache::Entry.write(@key, encode_entry(@value, version: @version.presence, expires_at: expires_at))
|
|
49
|
+
redirect_to entries_path, notice: "Entry created."
|
|
50
|
+
rescue InvalidExpiry
|
|
51
|
+
render_new_error("Couldn't parse the expiry time.")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def edit
|
|
55
|
+
decoded = decode_value(@entry.value)
|
|
56
|
+
value = resolved_value(decoded)
|
|
57
|
+
@value_editable = value.is_a?(String)
|
|
58
|
+
@meta_editable = value != UNREADABLE
|
|
59
|
+
@key = scrub_for_display(@entry.key)
|
|
60
|
+
@expires_at = format_expires_at(decoded)
|
|
61
|
+
@version = (decoded.version if decoded.respond_to?(:version)).to_s
|
|
62
|
+
|
|
63
|
+
if @value_editable
|
|
64
|
+
@value = value
|
|
65
|
+
elsif @meta_editable
|
|
66
|
+
@value = value.inspect
|
|
67
|
+
@note = "This entry holds a #{value.class} (not a plain string), so the value is " \
|
|
68
|
+
"read-only — but you can still change its metadata below."
|
|
69
|
+
else
|
|
70
|
+
@value = scrub_for_display(@entry.value)
|
|
71
|
+
@note = "This value couldn't be read as a cache entry (it may use a different cache format " \
|
|
72
|
+
"or reference a class this app can't load), so it can't be edited here."
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def update
|
|
77
|
+
value = resolved_value(decode_value(@entry.value))
|
|
78
|
+
if value == UNREADABLE
|
|
79
|
+
return redirect_to edit_entry_path(@entry), alert: "This entry can't be edited."
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
value = params[:value].to_s if value.is_a?(String)
|
|
83
|
+
expires_at = parse_expires_at(params[:expires_at])
|
|
84
|
+
SolidCache::Entry.write(@entry.key, encode_entry(value, version: params[:version].presence, expires_at: expires_at))
|
|
85
|
+
redirect_to entry_path(@entry), notice: "Entry updated."
|
|
86
|
+
rescue InvalidExpiry
|
|
87
|
+
redirect_to edit_entry_path(@entry), alert: "Couldn't parse the expiry time."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def destroy
|
|
91
|
+
@entry.destroy
|
|
92
|
+
redirect_to entries_path, notice: "Entry deleted."
|
|
11
93
|
end
|
|
12
94
|
|
|
13
95
|
def clear
|
|
@@ -17,8 +99,107 @@ module SolidWebUi::Cache
|
|
|
17
99
|
|
|
18
100
|
private
|
|
19
101
|
|
|
102
|
+
InvalidExpiry = Class.new(StandardError)
|
|
103
|
+
|
|
104
|
+
# Sentinel for a value that can't be read — either the entry didn't decode, or
|
|
105
|
+
# its envelope decoded but resolving the value raised (a LazyEntry resolves on
|
|
106
|
+
# first access and can fail, e.g. a Marshal value for a class this app can't load).
|
|
107
|
+
UNREADABLE = Object.new.freeze
|
|
108
|
+
private_constant :UNREADABLE
|
|
109
|
+
|
|
110
|
+
def set_entry
|
|
111
|
+
@entry = SolidCache::Entry.find(params[:id])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def render_new_error(message)
|
|
115
|
+
flash.now[:alert] = message
|
|
116
|
+
render :new, status: :unprocessable_entity
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Resolve a decoded entry's value, returning UNREADABLE if it can't be read.
|
|
120
|
+
def resolved_value(decoded)
|
|
121
|
+
return UNREADABLE unless decoded.respond_to?(:value)
|
|
122
|
+
|
|
123
|
+
decoded.value
|
|
124
|
+
rescue StandardError
|
|
125
|
+
UNREADABLE
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The cache store whose coder matches how these entries were written. Prefer the
|
|
129
|
+
# host's Rails.cache when it is the Solid Cache store (exact format match), and
|
|
130
|
+
# fall back to a default Solid Cache store otherwise.
|
|
131
|
+
def cache_store
|
|
132
|
+
@cache_store ||=
|
|
133
|
+
if defined?(SolidCache::Store) && Rails.cache.is_a?(SolidCache::Store)
|
|
134
|
+
Rails.cache
|
|
135
|
+
else
|
|
136
|
+
ActiveSupport::Cache.lookup_store(:solid_cache_store)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Expiry of a single entry for the index: epoch Float, :never, or :undecodable.
|
|
141
|
+
def entry_expiry(entry)
|
|
142
|
+
decoded = decode_value(entry.value)
|
|
143
|
+
return :undecodable unless decoded.respond_to?(:expires_at)
|
|
144
|
+
|
|
145
|
+
decoded.expires_at || :never
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Raw stored bytes -> ActiveSupport::Cache::Entry (or nil if undecodable).
|
|
149
|
+
def decode_value(raw)
|
|
150
|
+
cache_store.send(:deserialize_entry, raw.to_s)
|
|
151
|
+
rescue StandardError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# A value + metadata -> serialized cache-entry bytes.
|
|
156
|
+
def encode_entry(value, version:, expires_at:)
|
|
157
|
+
entry = ActiveSupport::Cache::Entry.new(value, version: version, expires_at: expires_at)
|
|
158
|
+
cache_store.send(:serialize_entry, entry)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def cache_time_zone
|
|
162
|
+
ActiveSupport::TimeZone[SolidWebUi::Cache.config.time_zone] || ActiveSupport::TimeZone["UTC"]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Absolute epoch on the decoded entry -> a datetime-local value (YYYY-MM-DDThh:mm:ss)
|
|
166
|
+
# in the dashboard's time zone.
|
|
167
|
+
def format_expires_at(decoded)
|
|
168
|
+
epoch = decoded.expires_at if decoded.respond_to?(:expires_at)
|
|
169
|
+
return "" unless epoch
|
|
170
|
+
|
|
171
|
+
Time.at(epoch).in_time_zone(cache_time_zone).strftime("%Y-%m-%dT%H:%M:%S")
|
|
172
|
+
rescue StandardError
|
|
173
|
+
"" # a corrupt/out-of-range epoch shouldn't break the form
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Form string (in the dashboard's time zone) -> a Time, or nil for "no expiry".
|
|
177
|
+
def parse_expires_at(str)
|
|
178
|
+
return nil if str.blank?
|
|
179
|
+
|
|
180
|
+
cache_time_zone.parse(str) || raise(InvalidExpiry)
|
|
181
|
+
rescue ArgumentError, RangeError, TypeError
|
|
182
|
+
raise InvalidExpiry
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def scrub_for_display(bytes)
|
|
186
|
+
bytes.to_s.dup.force_encoding("UTF-8").scrub("?")
|
|
187
|
+
end
|
|
188
|
+
|
|
20
189
|
def ensure_clear_enabled
|
|
21
190
|
head :forbidden unless SolidWebUi::Cache.config.enable_clear
|
|
22
191
|
end
|
|
192
|
+
|
|
193
|
+
def ensure_create_enabled
|
|
194
|
+
head :forbidden unless SolidWebUi::Cache.config.enable_create
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def ensure_edit_enabled
|
|
198
|
+
head :forbidden unless SolidWebUi::Cache.config.enable_edit
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def ensure_delete_enabled
|
|
202
|
+
head :forbidden unless SolidWebUi::Cache.config.enable_delete
|
|
203
|
+
end
|
|
23
204
|
end
|
|
24
205
|
end
|
|
@@ -20,5 +20,12 @@ module SolidWebUi::Queue
|
|
|
20
20
|
@paginator = SolidWebUi::Paginator.new(scope, page: params[:page], per_page: per_page)
|
|
21
21
|
@jobs = @paginator.records
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
def show
|
|
25
|
+
@job = SolidQueue::Job
|
|
26
|
+
.includes(:ready_execution, :scheduled_execution, :claimed_execution,
|
|
27
|
+
:blocked_execution, :failed_execution)
|
|
28
|
+
.find(params[:id])
|
|
29
|
+
end
|
|
23
30
|
end
|
|
24
31
|
end
|
|
@@ -9,8 +9,13 @@ module SolidWebUi::Cable
|
|
|
9
9
|
]
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def readable_channel(channel)
|
|
13
|
-
truncate(channel.to_s.dup.force_encoding("UTF-8").scrub("?"), length:
|
|
12
|
+
def readable_channel(channel, length: 80)
|
|
13
|
+
truncate(channel.to_s.dup.force_encoding("UTF-8").scrub("?"), length: length)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Cable payloads are binary; present a scrubbed, truncated preview.
|
|
17
|
+
def readable_payload(payload, length: 120)
|
|
18
|
+
truncate(payload.to_s.dup.force_encoding("UTF-8").scrub("?"), length: length)
|
|
14
19
|
end
|
|
15
20
|
|
|
16
21
|
def short_time(time)
|
|
@@ -10,8 +10,35 @@ module SolidWebUi::Cache
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
# Cache keys are stored as binary; present them as readable, truncated text.
|
|
13
|
-
def readable_key(key)
|
|
14
|
-
truncate(key.to_s.dup.force_encoding("UTF-8").scrub("?"), length:
|
|
13
|
+
def readable_key(key, length: 80)
|
|
14
|
+
truncate(key.to_s.dup.force_encoding("UTF-8").scrub("?"), length: length)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Cache values are binary (often a Marshal/compressed blob). Present a scrubbed,
|
|
18
|
+
# truncated preview so the show page never dumps megabytes of unreadable bytes.
|
|
19
|
+
def readable_value(value, length: 2000)
|
|
20
|
+
truncate(value.to_s.dup.force_encoding("UTF-8").scrub("?"), length: length)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Absolute expiry of a cache entry (see EntriesController#entry_expiry).
|
|
24
|
+
def cache_expires_at(expiry)
|
|
25
|
+
case expiry
|
|
26
|
+
when Numeric then short_time(Time.at(expiry))
|
|
27
|
+
when :never then "Never"
|
|
28
|
+
else "—"
|
|
29
|
+
end
|
|
30
|
+
rescue StandardError
|
|
31
|
+
"—" # a corrupt/out-of-range epoch shouldn't break the list
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Remaining time until a cache entry expires, e.g. "about 2 hours" / "expired".
|
|
35
|
+
def cache_time_to_expire(expiry)
|
|
36
|
+
return "—" unless expiry.is_a?(Numeric)
|
|
37
|
+
|
|
38
|
+
remaining = expiry - Time.now.to_f
|
|
39
|
+
return "expired" if remaining <= 0
|
|
40
|
+
|
|
41
|
+
distance_of_time_in_words(remaining)
|
|
15
42
|
end
|
|
16
43
|
|
|
17
44
|
def short_time(time)
|
|
@@ -29,6 +29,34 @@ module SolidWebUi::Queue
|
|
|
29
29
|
queue.respond_to?(:human_latency) ? queue.human_latency : queue.latency
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# Best available execution time for a job. Solid Queue does not persist a job's
|
|
33
|
+
# start time once it finishes (the claimed_execution row is deleted on finish),
|
|
34
|
+
# so:
|
|
35
|
+
# - finished job → finished_at - created_at (total time in the system)
|
|
36
|
+
# - claimed job → now - claimed_execution.created_at (live run time)
|
|
37
|
+
# - anything else → nil (no meaningful duration yet)
|
|
38
|
+
def job_duration(job)
|
|
39
|
+
if job.finished_at?
|
|
40
|
+
human_duration(job.finished_at - job.created_at)
|
|
41
|
+
elsif job.claimed_execution
|
|
42
|
+
human_duration(Time.current - job.claimed_execution.created_at)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Compact, sub-minute-precise duration: "1.2s", "3m 04s", "2h 01m".
|
|
47
|
+
def human_duration(seconds)
|
|
48
|
+
return "—" if seconds.nil?
|
|
49
|
+
|
|
50
|
+
seconds = seconds.to_f
|
|
51
|
+
return format("%.1fs", seconds) if seconds < 60
|
|
52
|
+
|
|
53
|
+
if seconds < 3600
|
|
54
|
+
format("%dm %02ds", seconds / 60, seconds % 60)
|
|
55
|
+
else
|
|
56
|
+
format("%dh %02dm", seconds / 3600, (seconds % 3600) / 60)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
32
60
|
def short_time(time)
|
|
33
61
|
return "—" if time.nil?
|
|
34
62
|
|
|
@@ -8,6 +8,18 @@
|
|
|
8
8
|
<%= solid_web_ui_head_tags %>
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
|
+
<%# Flash lives in the gem's own layout only. When the dashboards are embedded %>
|
|
12
|
+
<%# in a host layout (config.layout = "your_layout"), the host renders flash, so %>
|
|
13
|
+
<%# rendering it here too would duplicate it. %>
|
|
14
|
+
<% if flash.any? %>
|
|
15
|
+
<div class="solid-web-ui" data-color-scheme="<%= SolidWebUi.config.color_scheme %>">
|
|
16
|
+
<div class="swui-flash-region">
|
|
17
|
+
<% flash.each do |type, message| %>
|
|
18
|
+
<div class="swui-flash swui-flash--<%= type.to_s == "alert" ? "alert" : "notice" %>"><%= message %></div>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
11
23
|
<%= yield %>
|
|
12
24
|
</body>
|
|
13
25
|
</html>
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
<%= swui_page(title: "Channels", nav: cable_nav(:channels)) do %>
|
|
3
3
|
<%= swui_table(headers: [ "Channel", "Messages", "Last activity" ], empty_message: "No channels.") do %>
|
|
4
4
|
<% @channels.each do |row| %>
|
|
5
|
-
<tr>
|
|
6
|
-
<td
|
|
5
|
+
<tr data-swui-row-href="<%= channel_path(row[:hash]) %>">
|
|
6
|
+
<td><%= link_to channel_path(row[:hash]), data: { turbo_frame: "_top" } do %><code><%= readable_channel(row[:name]) %></code><% end %></td>
|
|
7
7
|
<td><%= number_with_delimiter(row[:count]) %></td>
|
|
8
8
|
<td><%= short_time(row[:last]) %></td>
|
|
9
9
|
</tr>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<% title = readable_channel(@name) %>
|
|
2
|
+
<% content_for :title, "Channel #{title}" %>
|
|
3
|
+
<%= swui_page(title: "Channel", nav: cable_nav(:channels), refresh: false) do %>
|
|
4
|
+
<%= link_to "← Back to channels", channels_path, class: "swui-back" %>
|
|
5
|
+
|
|
6
|
+
<dl class="swui-detail">
|
|
7
|
+
<dt>Channel</dt>
|
|
8
|
+
<dd><code><%= readable_channel(@name, length: 1024) %></code></dd>
|
|
9
|
+
|
|
10
|
+
<dt>Channel hash</dt>
|
|
11
|
+
<dd><code><%= @channel_hash %></code></dd>
|
|
12
|
+
|
|
13
|
+
<dt>Messages</dt>
|
|
14
|
+
<dd><%= number_with_delimiter(@count) %></dd>
|
|
15
|
+
|
|
16
|
+
<dt>First activity</dt>
|
|
17
|
+
<dd><%= short_time(@first) %></dd>
|
|
18
|
+
|
|
19
|
+
<dt>Last activity</dt>
|
|
20
|
+
<dd><%= short_time(@last) %></dd>
|
|
21
|
+
</dl>
|
|
22
|
+
|
|
23
|
+
<h2 class="swui-section-title">Recent messages</h2>
|
|
24
|
+
<%= swui_table(headers: [ "ID", "Payload", "Created" ], empty_message: "No messages.") do %>
|
|
25
|
+
<% @messages.each do |message| %>
|
|
26
|
+
<tr data-swui-row-href="<%= message_path(message) %>">
|
|
27
|
+
<td><%= link_to message.id, message_path(message) %></td>
|
|
28
|
+
<td><code><%= readable_payload(message.payload) %></code></td>
|
|
29
|
+
<td><%= short_time(message.created_at) %></td>
|
|
30
|
+
</tr>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<%= swui_paginator(paginator: @paginator, page_url: ->(page) { channel_path(@channel_hash, page: page) }) %>
|
|
35
|
+
<% end %>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<% content_for :title, "Message ##{@message.id}" %>
|
|
2
|
+
<%= swui_page(title: "Message ##{@message.id}", nav: cable_nav(:channels), refresh: false) do %>
|
|
3
|
+
<%= link_to "← Back to channel", channel_path(@message.channel_hash), class: "swui-back" %>
|
|
4
|
+
|
|
5
|
+
<dl class="swui-detail">
|
|
6
|
+
<dt>ID</dt>
|
|
7
|
+
<dd><%= @message.id %></dd>
|
|
8
|
+
|
|
9
|
+
<dt>Channel</dt>
|
|
10
|
+
<dd><code><%= readable_channel(@message.channel, length: 1024) %></code></dd>
|
|
11
|
+
|
|
12
|
+
<dt>Channel hash</dt>
|
|
13
|
+
<dd><code><%= @message.channel_hash %></code></dd>
|
|
14
|
+
|
|
15
|
+
<dt>Created</dt>
|
|
16
|
+
<dd><%= short_time(@message.created_at) %></dd>
|
|
17
|
+
|
|
18
|
+
<dt>Payload</dt>
|
|
19
|
+
<dd><pre class="swui-codeblock"><%= readable_payload(@message.payload, length: 100_000) %></pre></dd>
|
|
20
|
+
</dl>
|
|
21
|
+
<% end %>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<%= form_with url: url, method: method, class: "swui-form" do |f| %>
|
|
2
|
+
<div class="swui-field">
|
|
3
|
+
<label class="swui-label" for="entry_key">Key</label>
|
|
4
|
+
<% if new_record %>
|
|
5
|
+
<%= text_field_tag :key, key, id: "entry_key", class: "swui-input", autocomplete: "off" %>
|
|
6
|
+
<% else %>
|
|
7
|
+
<input id="entry_key" class="swui-input" value="<%= key %>" disabled>
|
|
8
|
+
<p class="swui-hint">The key identifies the entry and can't be changed — delete and recreate to rename.</p>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="swui-field">
|
|
13
|
+
<label class="swui-label" for="entry_value">Value</label>
|
|
14
|
+
<% if note %>
|
|
15
|
+
<div class="swui-flash swui-flash--alert"><%= note %></div>
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= text_area_tag :value, value, id: "entry_value", class: "swui-textarea", rows: 12, disabled: !value_editable %>
|
|
18
|
+
<% if value_editable %>
|
|
19
|
+
<p class="swui-hint">Saved through the cache encoder, so <code>Rails.cache</code> reads it back
|
|
20
|
+
as a string.</p>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<fieldset class="swui-fieldset">
|
|
25
|
+
<legend class="swui-label">Metadata</legend>
|
|
26
|
+
<div class="swui-field">
|
|
27
|
+
<label class="swui-label" for="entry_expires_at">Expires at</label>
|
|
28
|
+
<%= text_field_tag :expires_at, expires_at, id: "entry_expires_at", class: "swui-input",
|
|
29
|
+
type: "datetime-local", step: 1, disabled: !meta_editable %>
|
|
30
|
+
<p class="swui-hint">Time zone <code><%= SolidWebUi::Cache.config.time_zone %></code>. Leave blank for no expiry.</p>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="swui-field">
|
|
34
|
+
<label class="swui-label" for="entry_version">Version</label>
|
|
35
|
+
<%= text_field_tag :version, version, id: "entry_version", class: "swui-input",
|
|
36
|
+
autocomplete: "off", disabled: !meta_editable %>
|
|
37
|
+
<p class="swui-hint">Used by <code>Rails.cache</code>'s <code>:version</code> option. Leave blank for none.</p>
|
|
38
|
+
</div>
|
|
39
|
+
</fieldset>
|
|
40
|
+
|
|
41
|
+
<div class="swui-form__actions">
|
|
42
|
+
<% if meta_editable %>
|
|
43
|
+
<%= f.submit(new_record ? "Create entry" : "Save changes", class: "swui-btn") %>
|
|
44
|
+
<% end %>
|
|
45
|
+
<%= link_to "Cancel", cancel_url, class: "swui-btn" %>
|
|
46
|
+
</div>
|
|
47
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% content_for :title, "Edit cache entry ##{@entry.id}" %>
|
|
2
|
+
<%= swui_page(title: "Edit cache entry ##{@entry.id}", nav: cache_nav(:entries), refresh: false) do %>
|
|
3
|
+
<%= link_to "← Back to entry", entry_path(@entry), class: "swui-back" %>
|
|
4
|
+
|
|
5
|
+
<%= render "form", url: entry_path(@entry), method: :patch, new_record: false,
|
|
6
|
+
value_editable: @value_editable, meta_editable: @meta_editable, note: @note,
|
|
7
|
+
cancel_url: entry_path(@entry),
|
|
8
|
+
key: @key, value: @value, expires_at: @expires_at, version: @version %>
|
|
9
|
+
<% end %>
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
<% content_for :title, "Cache entries" %>
|
|
2
2
|
<%= swui_page(title: "Cache entries", nav: cache_nav(:entries)) do %>
|
|
3
|
-
|
|
3
|
+
<% if SolidWebUi::Cache.config.enable_create %>
|
|
4
|
+
<div class="swui-actions">
|
|
5
|
+
<%= link_to "+ New entry", new_entry_path, class: "swui-btn", data: { turbo_frame: "_top" } %>
|
|
6
|
+
</div>
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<% headers = [ "ID", "Key", "Size", "Created", "Expires", "Time to expire" ] %>
|
|
10
|
+
<% headers << "" if SolidWebUi::Cache.config.enable_delete %>
|
|
11
|
+
<%= swui_table(headers: headers, empty_message: "The cache is empty.") do %>
|
|
4
12
|
<% @entries.each do |entry| %>
|
|
5
|
-
<tr>
|
|
6
|
-
<td><%= entry.id %></td>
|
|
13
|
+
<tr data-swui-row-href="<%= entry_path(entry) %>">
|
|
14
|
+
<td><%= link_to entry.id, entry_path(entry), data: { turbo_frame: "_top" } %></td>
|
|
7
15
|
<td><code><%= readable_key(entry.key) %></code></td>
|
|
8
16
|
<td><%= number_to_human_size(entry.byte_size) %></td>
|
|
9
17
|
<td><%= short_time(entry.created_at) %></td>
|
|
18
|
+
<td><%= cache_expires_at(@expiry[entry.id]) %></td>
|
|
19
|
+
<td><%= cache_time_to_expire(@expiry[entry.id]) %></td>
|
|
20
|
+
<% if SolidWebUi::Cache.config.enable_delete %>
|
|
21
|
+
<td><%= swui_action_button(label: "Delete", url: entry_path(entry), method: :delete,
|
|
22
|
+
danger: true, confirm: "Delete this cache entry?") %></td>
|
|
23
|
+
<% end %>
|
|
10
24
|
</tr>
|
|
11
25
|
<% end %>
|
|
12
26
|
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<% content_for :title, "New cache entry" %>
|
|
2
|
+
<%= swui_page(title: "New cache entry", nav: cache_nav(:entries), refresh: false) do %>
|
|
3
|
+
<%= link_to "← Back to entries", entries_path, class: "swui-back" %>
|
|
4
|
+
|
|
5
|
+
<%= render "form", url: entries_path, method: :post, new_record: true,
|
|
6
|
+
value_editable: true, meta_editable: true, note: nil, cancel_url: entries_path,
|
|
7
|
+
key: @key, value: @value, expires_at: @expires_at, version: @version %>
|
|
8
|
+
<% end %>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<% content_for :title, "Cache entry ##{@entry.id}" %>
|
|
2
|
+
<%= swui_page(title: "Cache entry ##{@entry.id}", nav: cache_nav(:entries), refresh: false) do %>
|
|
3
|
+
<%= link_to "← Back to entries", entries_path, class: "swui-back" %>
|
|
4
|
+
|
|
5
|
+
<dl class="swui-detail">
|
|
6
|
+
<dt>ID</dt>
|
|
7
|
+
<dd><%= @entry.id %></dd>
|
|
8
|
+
|
|
9
|
+
<dt>Key</dt>
|
|
10
|
+
<dd><code><%= readable_key(@entry.key, length: 1024) %></code></dd>
|
|
11
|
+
|
|
12
|
+
<dt>Key hash</dt>
|
|
13
|
+
<dd><code><%= @entry.key_hash %></code></dd>
|
|
14
|
+
|
|
15
|
+
<dt>Size</dt>
|
|
16
|
+
<dd><%= number_to_human_size(@entry.byte_size) %> (<%= number_with_delimiter(@entry.byte_size) %> bytes)</dd>
|
|
17
|
+
|
|
18
|
+
<dt>Created</dt>
|
|
19
|
+
<dd><%= short_time(@entry.created_at) %></dd>
|
|
20
|
+
|
|
21
|
+
<% if @decoded.respond_to?(:expires_at) %>
|
|
22
|
+
<dt>Expires</dt>
|
|
23
|
+
<dd><%= @decoded.expires_at ? short_time(Time.at(@decoded.expires_at)) : "Never" %></dd>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<% if @decoded.respond_to?(:version) %>
|
|
27
|
+
<dt>Version</dt>
|
|
28
|
+
<dd><%= @decoded.version.presence || "—" %></dd>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<dt>Value<% unless @value_readable %> <span class="swui-detail__hint">(raw — unreadable)</span><% end %></dt>
|
|
32
|
+
<dd>
|
|
33
|
+
<% if @value_readable %>
|
|
34
|
+
<pre class="swui-codeblock"><%= @value_text %></pre>
|
|
35
|
+
<% else %>
|
|
36
|
+
<pre class="swui-codeblock"><%= readable_value(@entry.value) %></pre>
|
|
37
|
+
<% end %>
|
|
38
|
+
</dd>
|
|
39
|
+
</dl>
|
|
40
|
+
|
|
41
|
+
<div class="swui-actions">
|
|
42
|
+
<% if SolidWebUi::Cache.config.enable_edit %>
|
|
43
|
+
<%= link_to "Edit", edit_entry_path(@entry), class: "swui-btn", data: { turbo_frame: "_top" } %>
|
|
44
|
+
<% end %>
|
|
45
|
+
<% if SolidWebUi::Cache.config.enable_delete %>
|
|
46
|
+
<%= swui_action_button(label: "Delete", url: entry_path(@entry), method: :delete,
|
|
47
|
+
danger: true, confirm: "Delete this cache entry?") %>
|
|
48
|
+
<% end %>
|
|
49
|
+
</div>
|
|
50
|
+
<% end %>
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
<%= swui_table(headers: [ "ID", "Job", "Queue", "Status", "Created" ], empty_message: "No jobs.") do %>
|
|
11
11
|
<% @jobs.each do |job| %>
|
|
12
|
-
<tr>
|
|
13
|
-
<td><%= job.id %></td>
|
|
12
|
+
<tr data-swui-row-href="<%= job_path(job) %>">
|
|
13
|
+
<td><%= link_to job.id, job_path(job), data: { turbo_frame: "_top" } %></td>
|
|
14
14
|
<td><%= job.class_name %></td>
|
|
15
15
|
<td><%= job.queue_name %></td>
|
|
16
16
|
<td><%= swui_status_badge(label: job_status(job).to_s.humanize, status: job_status(job)) %></td>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<% content_for :title, "Job ##{@job.id}" %>
|
|
2
|
+
<%= swui_page(title: "Job ##{@job.id}", nav: queue_nav(:jobs), refresh: false) do %>
|
|
3
|
+
<%= link_to "← Back to jobs", jobs_path, class: "swui-back" %>
|
|
4
|
+
|
|
5
|
+
<dl class="swui-detail">
|
|
6
|
+
<dt>ID</dt>
|
|
7
|
+
<dd><%= @job.id %></dd>
|
|
8
|
+
|
|
9
|
+
<dt>Job</dt>
|
|
10
|
+
<dd><%= @job.class_name %></dd>
|
|
11
|
+
|
|
12
|
+
<dt>Queue</dt>
|
|
13
|
+
<dd><%= @job.queue_name %></dd>
|
|
14
|
+
|
|
15
|
+
<dt>Status</dt>
|
|
16
|
+
<dd><%= swui_status_badge(label: job_status(@job).to_s.humanize, status: job_status(@job)) %></dd>
|
|
17
|
+
|
|
18
|
+
<dt>Priority</dt>
|
|
19
|
+
<dd><%= @job.priority %></dd>
|
|
20
|
+
|
|
21
|
+
<dt>Active Job ID</dt>
|
|
22
|
+
<dd><code><%= @job.active_job_id || "—" %></code></dd>
|
|
23
|
+
|
|
24
|
+
<dt>Enqueued</dt>
|
|
25
|
+
<dd><%= short_time(@job.created_at) %></dd>
|
|
26
|
+
|
|
27
|
+
<dt>Scheduled</dt>
|
|
28
|
+
<dd><%= short_time(@job.scheduled_at) %></dd>
|
|
29
|
+
|
|
30
|
+
<dt>Finished</dt>
|
|
31
|
+
<dd><%= short_time(@job.finished_at) %></dd>
|
|
32
|
+
|
|
33
|
+
<dt>Duration<% if @job.finished_at? %> <span class="swui-detail__hint">(enqueue → finish)</span><% end %></dt>
|
|
34
|
+
<dd><%= job_duration(@job) || "—" %></dd>
|
|
35
|
+
|
|
36
|
+
<dt>Arguments</dt>
|
|
37
|
+
<dd><pre class="swui-codeblock"><%= JSON.pretty_generate(@job.arguments) rescue @job.arguments.inspect %></pre></dd>
|
|
38
|
+
</dl>
|
|
39
|
+
|
|
40
|
+
<% if (failed = @job.failed_execution) %>
|
|
41
|
+
<h2 class="swui-section-title">Error</h2>
|
|
42
|
+
<dl class="swui-detail">
|
|
43
|
+
<dt>Exception</dt>
|
|
44
|
+
<dd><%= swui_status_badge(label: failed.exception_class.to_s, status: :failed) %></dd>
|
|
45
|
+
|
|
46
|
+
<dt>Message</dt>
|
|
47
|
+
<dd><%= failed.message %></dd>
|
|
48
|
+
|
|
49
|
+
<dt>Failed at</dt>
|
|
50
|
+
<dd><%= short_time(failed.created_at) %></dd>
|
|
51
|
+
|
|
52
|
+
<dt>Backtrace</dt>
|
|
53
|
+
<dd>
|
|
54
|
+
<% if failed.backtrace.present? %>
|
|
55
|
+
<pre class="swui-codeblock"><%= Array(failed.backtrace).join("\n") %></pre>
|
|
56
|
+
<% else %>
|
|
57
|
+
—
|
|
58
|
+
<% end %>
|
|
59
|
+
</dd>
|
|
60
|
+
</dl>
|
|
61
|
+
<% end %>
|
|
62
|
+
<% end %>
|
|
@@ -3,5 +3,7 @@
|
|
|
3
3
|
SolidWebUi::Cable::Engine.routes.draw do
|
|
4
4
|
root to: "dashboard#index"
|
|
5
5
|
get "channels", to: "channels#index", as: :channels
|
|
6
|
+
get "channels/:channel_hash", to: "channels#show", as: :channel
|
|
6
7
|
delete "messages/trim", to: "messages#trim", as: :trim_messages
|
|
8
|
+
get "messages/:id", to: "messages#show", as: :message
|
|
7
9
|
end
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
SolidWebUi::Cache::Engine.routes.draw do
|
|
4
4
|
root to: "dashboard#index"
|
|
5
|
+
|
|
5
6
|
get "entries", to: "entries#index", as: :entries
|
|
7
|
+
get "entries/new", to: "entries#new", as: :new_entry
|
|
8
|
+
post "entries", to: "entries#create"
|
|
6
9
|
delete "entries", to: "entries#clear", as: :clear_entries
|
|
10
|
+
|
|
11
|
+
get "entries/:id", to: "entries#show", as: :entry
|
|
12
|
+
get "entries/:id/edit", to: "entries#edit", as: :edit_entry
|
|
13
|
+
patch "entries/:id", to: "entries#update"
|
|
14
|
+
delete "entries/:id", to: "entries#destroy", as: :delete_entry
|
|
7
15
|
end
|
data/lib/solid_web_ui/cache.rb
CHANGED
|
@@ -8,6 +8,7 @@ SolidWebUi::Queue::Engine.routes.draw do
|
|
|
8
8
|
post "queues/:name/resume", to: "queues#resume", as: :resume_queue, constraints: { name: %r{[^/]+} }
|
|
9
9
|
|
|
10
10
|
get "jobs", to: "jobs#index", as: :jobs
|
|
11
|
+
get "jobs/:id", to: "jobs#show", as: :job
|
|
11
12
|
|
|
12
13
|
get "failed", to: "failed_executions#index", as: :failed_executions
|
|
13
14
|
post "failed/:id/retry", to: "failed_executions#retry", as: :retry_failed_execution
|
data/lib/solid_web_ui/theme.rb
CHANGED
|
@@ -21,7 +21,10 @@ module SolidWebUi
|
|
|
21
21
|
color_warning: [ "--swui-color-warning", "var(--color-warning, #d97706)" ],
|
|
22
22
|
color_danger: [ "--swui-color-danger", "var(--color-danger, #dc2626)" ],
|
|
23
23
|
font: [ "--swui-font", "var(--swui-host-font, ui-sans-serif, system-ui, sans-serif)" ],
|
|
24
|
-
radius: [ "--swui-radius", "0.5rem" ]
|
|
24
|
+
radius: [ "--swui-radius", "0.5rem" ],
|
|
25
|
+
# Max width of the centered page column. Set to "none" or "100%" to let the
|
|
26
|
+
# dashboards fill their container (useful when embedded in a host layout).
|
|
27
|
+
page_max_width: [ "--swui-page-max-width", "72rem" ]
|
|
25
28
|
}.freeze
|
|
26
29
|
|
|
27
30
|
# Dark scheme only re-points the surface/text family; brand color stays.
|
data/lib/solid_web_ui/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_web_ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anton Radushev
|
|
@@ -151,12 +151,19 @@ files:
|
|
|
151
151
|
- app/helpers/solid_web_ui/queue/application_helper.rb
|
|
152
152
|
- app/views/layouts/solid_web_ui.html.erb
|
|
153
153
|
- app/views/solid_web_ui/cable/channels/index.html.erb
|
|
154
|
+
- app/views/solid_web_ui/cable/channels/show.html.erb
|
|
154
155
|
- app/views/solid_web_ui/cable/dashboard/index.html.erb
|
|
156
|
+
- app/views/solid_web_ui/cable/messages/show.html.erb
|
|
155
157
|
- app/views/solid_web_ui/cache/dashboard/index.html.erb
|
|
158
|
+
- app/views/solid_web_ui/cache/entries/_form.html.erb
|
|
159
|
+
- app/views/solid_web_ui/cache/entries/edit.html.erb
|
|
156
160
|
- app/views/solid_web_ui/cache/entries/index.html.erb
|
|
161
|
+
- app/views/solid_web_ui/cache/entries/new.html.erb
|
|
162
|
+
- app/views/solid_web_ui/cache/entries/show.html.erb
|
|
157
163
|
- app/views/solid_web_ui/queue/dashboard/index.html.erb
|
|
158
164
|
- app/views/solid_web_ui/queue/failed_executions/index.html.erb
|
|
159
165
|
- app/views/solid_web_ui/queue/jobs/index.html.erb
|
|
166
|
+
- app/views/solid_web_ui/queue/jobs/show.html.erb
|
|
160
167
|
- app/views/solid_web_ui/queue/processes/index.html.erb
|
|
161
168
|
- app/views/solid_web_ui/queue/queues/index.html.erb
|
|
162
169
|
- app/views/solid_web_ui/queue/recurring_tasks/index.html.erb
|