solid_stack_web 0.1.0 → 0.2.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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -2
  3. data/app/assets/stylesheets/solid_stack_web/_04_table.css +1 -0
  4. data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +13 -0
  5. data/app/assets/stylesheets/solid_stack_web/_08_filters.css +59 -0
  6. data/app/assets/stylesheets/solid_stack_web/_09_detail.css +85 -0
  7. data/app/controllers/solid_stack_web/application_controller.rb +4 -0
  8. data/app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb +17 -0
  9. data/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +22 -0
  10. data/app/controllers/solid_stack_web/failed_jobs_controller.rb +35 -4
  11. data/app/controllers/solid_stack_web/jobs/selections_controller.rb +24 -0
  12. data/app/controllers/solid_stack_web/jobs_controller.rb +60 -21
  13. data/app/javascript/solid_stack_web/application.js +6 -0
  14. data/app/javascript/solid_stack_web/selection_controller.js +42 -0
  15. data/app/models/solid_stack_web/job.rb +21 -0
  16. data/app/views/layouts/solid_stack_web/application.html.erb +1 -0
  17. data/app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb +9 -0
  18. data/app/views/solid_stack_web/failed_jobs/index.html.erb +61 -30
  19. data/app/views/solid_stack_web/failed_jobs/show.html.erb +58 -0
  20. data/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb +2 -2
  21. data/app/views/solid_stack_web/jobs/index.html.erb +117 -34
  22. data/app/views/solid_stack_web/jobs/show.html.erb +57 -0
  23. data/config/importmap.rb +2 -0
  24. data/config/routes.rb +10 -2
  25. data/lib/solid_stack_web/engine.rb +15 -0
  26. data/lib/solid_stack_web/version.rb +1 -1
  27. metadata +55 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ff5e759d441ff203317f09e35d4e9fb6876f81b544d4b075093c3d2cf397293
4
- data.tar.gz: 9be02459700b7da623c939241ffc49e0147a4a460fb68f85f0ff3d63b10cea7f
3
+ metadata.gz: 0adcafbdcb32ea6e96f982efc8d44502ea31e75e9f25138558ba58a8952719c8
4
+ data.tar.gz: a0e5b5695d620e4d616535e5f7b394df10c756ee712584a7de7c7ef489ded759
5
5
  SHA512:
6
- metadata.gz: 8dcdb3c9445a05628849c51d6081b5a3f399249b690da8a254583e134b0d761b04a9d9228818614d1b0239d4226648cec75efd8da6c7b8a990f0832df57730a4
7
- data.tar.gz: ea5a60256af380188021c58aeeab6ab7e58c8eea3bd5aaf9ff03c3306d763cebb667fef58482e09fc1ee677894b110f03ed6cadb989bc254b6cb73d997909a52
6
+ metadata.gz: 42c74a1cbc74acf3485ce0649e7f894bbbc1fdb4172037dbac4edb6e6805c8571c3731efb5acaa774763ae979ae8bb632e81e16f7f36e51faed447c8eb3bd81d
7
+ data.tar.gz: e35fb81b13d2f4cfa2d43f2e9ee223ef32c9605a69c98cf38e94f193d037f284efae651922ace035e04ad0ac1129361825345d5a64b97d91809caad1201351b9
data/README.md CHANGED
@@ -9,8 +9,9 @@ A mountable Rails engine that provides a unified web dashboard for the full [Sol
9
9
 
10
10
  ## Features
11
11
 
12
- - **Overview dashboard** with live counts across all three Solid Stack components
13
- - **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked), manage failed jobs (retry / discard), pause/resume queues, and inspect worker processes
12
+ - **Overview dashboard** with live counts across all three Solid Stack components; cards are clickable and link directly to each section
13
+ - **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked) with filtering by job class, queue name, priority, and time period; manage failed jobs (retry / discard / bulk retry / bulk discard), pause/resume queues, and inspect worker processes; **Bulk selection** checkbox-selects individual jobs for discard or retry; **Discard All** bulk-discards every job matching the current filters in one request; **CSV export** downloads jobs or failed jobs as a CSV file respecting active filters
14
+ - **Job detail page** — drill into any job to see full arguments (pretty-printed JSON), queue, priority, enqueued time, Active Job ID, concurrency key, scheduled/blocked-until metadata, and a Discard button
14
15
  - **Solid Cache** — entry count and total byte size at a glance
15
16
  - **Solid Cable** — active message count and distinct channel count
16
17
  - **Turbo Stream** job discard — removes the row inline without a full page reload
@@ -56,6 +57,19 @@ SolidStackWeb.configure do |config|
56
57
  end
57
58
  ```
58
59
 
60
+ ### Job Filtering
61
+
62
+ The jobs list supports four independent filters, all driven by query params:
63
+
64
+ | Param | Description |
65
+ |-------|-------------|
66
+ | `q` | Substring match against the job class name (e.g. `q=Report`) |
67
+ | `queue` | Exact queue name match; select appears only when multiple queues exist |
68
+ | `priority` | Exact priority value match; select appears only when multiple priorities exist |
69
+ | `period` | Enqueued-at window — `1h`, `24h`, `7d`, or omit for all time |
70
+
71
+ Filters are preserved when switching between status tabs (Ready / Scheduled / Running / Blocked) and when discarding a job. They can be combined freely.
72
+
59
73
  ### Authentication
60
74
 
61
75
  The `authenticate` block is evaluated in the context of each request's controller instance, so any helper method available to controllers (e.g. `current_user` from Devise) works directly. If the block returns `false` or `nil`, the engine falls back to HTTP Basic authentication. If no `authenticate` block is configured, the dashboard is open.
@@ -67,6 +81,7 @@ The `authenticate` block is evaluated in the context of each request's controlle
67
81
  - [solid_queue](https://github.com/rails/solid_queue) >= 1.0
68
82
  - [solid_cache](https://github.com/rails/solid_cache) >= 1.0
69
83
  - [solid_cable](https://github.com/rails/solid_cable) >= 1.0
84
+ - [turbo-rails](https://github.com/hotwired/turbo-rails) >= 2.0
70
85
 
71
86
  ## Contributing
72
87
 
@@ -28,6 +28,7 @@
28
28
  .sqw-table tbody tr:hover { background: #f9fafb; }
29
29
 
30
30
  .sqw-actions { text-align: right; white-space: nowrap; }
31
+ .sqw-actions form { display: inline; }
31
32
 
32
33
  .sqw-empty {
33
34
  background: var(--surface);
@@ -12,7 +12,10 @@
12
12
  border-radius: var(--radius);
13
13
  box-shadow: var(--shadow);
14
14
  overflow: hidden;
15
+ position: relative;
16
+ transition: box-shadow 0.15s;
15
17
  }
18
+ .sqw-gem-card:hover { box-shadow: 0 3px 8px rgba(0,0,0,.12); }
16
19
 
17
20
  .sqw-gem-card--queue { border-top-color: var(--primary); }
18
21
  .sqw-gem-card--cache { border-top-color: var(--purple); }
@@ -41,6 +44,16 @@
41
44
  }
42
45
  .sqw-gem-card__link:hover { color: var(--primary); text-decoration: none; }
43
46
 
47
+ /* Stretch the header link to cover the whole card */
48
+ .sqw-gem-card__link::after {
49
+ content: "";
50
+ position: absolute;
51
+ inset: 0;
52
+ }
53
+
54
+ /* Stat links and other interactive elements sit above the overlay */
55
+ .sqw-inline-stat { position: relative; z-index: 1; }
56
+
44
57
  .sqw-gem-card__body {
45
58
  display: grid;
46
59
  grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
@@ -0,0 +1,59 @@
1
+ .sqw-filters {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 0.5rem;
5
+ flex-wrap: wrap;
6
+ margin-bottom: 1rem;
7
+ }
8
+
9
+ .sqw-search-input {
10
+ padding: 0.35rem 0.75rem;
11
+ font-size: 13px;
12
+ border: 1px solid var(--border);
13
+ border-radius: var(--radius);
14
+ background: var(--surface);
15
+ color: var(--text);
16
+ min-width: 200px;
17
+ }
18
+ .sqw-search-input:focus {
19
+ outline: 2px solid var(--primary);
20
+ outline-offset: -1px;
21
+ }
22
+
23
+ .sqw-select {
24
+ padding: 0.35rem 0.6rem;
25
+ font-size: 13px;
26
+ border: 1px solid var(--border);
27
+ border-radius: var(--radius);
28
+ background: var(--surface);
29
+ color: var(--text);
30
+ cursor: pointer;
31
+ }
32
+
33
+ .sqw-period-filter {
34
+ display: flex;
35
+ border: 1px solid var(--border);
36
+ border-radius: var(--radius);
37
+ overflow: hidden;
38
+ margin-left: auto;
39
+ }
40
+
41
+ .sqw-period-btn {
42
+ padding: 0.35rem 0.65rem;
43
+ font-size: 13px;
44
+ font-weight: 500;
45
+ color: var(--muted);
46
+ background: var(--surface);
47
+ }
48
+ .sqw-period-btn + .sqw-period-btn {
49
+ border-left: 1px solid var(--border);
50
+ }
51
+ .sqw-period-btn:hover:not(.sqw-period-btn--active) {
52
+ background: var(--bg);
53
+ color: var(--text);
54
+ text-decoration: none;
55
+ }
56
+ .sqw-period-btn--active {
57
+ background: var(--primary);
58
+ color: #fff;
59
+ }
@@ -0,0 +1,85 @@
1
+ .sqw-detail-card {
2
+ background: var(--surface);
3
+ border: 1px solid var(--border);
4
+ border-radius: var(--radius);
5
+ box-shadow: var(--shadow);
6
+ overflow: hidden;
7
+ }
8
+
9
+ .sqw-breadcrumb {
10
+ font-size: 12px;
11
+ color: var(--muted);
12
+ margin-bottom: 0.25rem;
13
+ }
14
+ .sqw-breadcrumb a { color: var(--muted); text-decoration: none; }
15
+ .sqw-breadcrumb a:hover { color: var(--text); }
16
+
17
+ .sqw-page-header--split {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: space-between;
21
+ }
22
+
23
+ .sqw-detail-actions,
24
+ .sqw-header-actions {
25
+ display: flex;
26
+ gap: 0.5rem;
27
+ }
28
+
29
+ /* Two-column detail layout: Details card | Arguments card */
30
+ .sqw-detail-grid {
31
+ display: grid;
32
+ grid-template-columns: 1fr 1fr;
33
+ gap: 1.5rem;
34
+ }
35
+
36
+ .sqw-detail-section {
37
+ padding: 1.25rem;
38
+ }
39
+
40
+ .sqw-section-title {
41
+ font-size: 13px;
42
+ font-weight: 600;
43
+ text-transform: uppercase;
44
+ letter-spacing: .05em;
45
+ color: var(--muted);
46
+ margin-bottom: 0.75rem;
47
+ }
48
+
49
+ /* Definition list — dt naturally sized, dd takes the rest */
50
+ .sqw-dl {
51
+ display: grid;
52
+ grid-template-columns: auto 1fr;
53
+ gap: 0.5rem 1.5rem;
54
+ font-size: 13px;
55
+ }
56
+ .sqw-dl dt { color: var(--muted); white-space: nowrap; }
57
+ .sqw-dl dd { word-break: break-all; }
58
+
59
+ .sqw-code-block {
60
+ font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
61
+ font-size: 12px;
62
+ background: var(--bg);
63
+ border: 1px solid var(--border);
64
+ border-radius: var(--radius);
65
+ padding: 0.75rem;
66
+ overflow-x: auto;
67
+ white-space: pre-wrap;
68
+ word-break: break-word;
69
+ max-height: 400px;
70
+ overflow-y: auto;
71
+ }
72
+
73
+ .sqw-code-input {
74
+ font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
75
+ font-size: 12px;
76
+ background: var(--bg);
77
+ border: 1px solid var(--border);
78
+ border-radius: var(--radius);
79
+ padding: 0.75rem;
80
+ width: 100%;
81
+ resize: vertical;
82
+ color: var(--text);
83
+ line-height: 1.5;
84
+ box-sizing: border-box;
85
+ }
@@ -1,7 +1,11 @@
1
+ require "csv"
2
+
1
3
  module SolidStackWeb
2
4
  class ApplicationController < ActionController::Base
3
5
  include Pagy::Method
4
6
 
7
+ PERIOD_DURATIONS = { "1h" => 1.hour, "24h" => 24.hours, "7d" => 7.days }.freeze
8
+
5
9
  before_action :authenticate!
6
10
  around_action :with_database_connection
7
11
 
@@ -0,0 +1,17 @@
1
+ module SolidStackWeb
2
+ module FailedJobs
3
+ class ArgumentsController < ApplicationController
4
+ def update
5
+ @execution = SolidQueue::FailedExecution.includes(:job).find(params[:failed_job_id])
6
+ new_arguments = JSON.parse(params[:arguments])
7
+ @execution.job.update!(arguments: new_arguments)
8
+ @execution.retry
9
+ redirect_to failed_jobs_path, notice: "Arguments updated and job queued for retry."
10
+ rescue JSON::ParserError
11
+ redirect_to failed_job_path(@execution), alert: "Invalid JSON — arguments were not saved."
12
+ rescue => e
13
+ redirect_to failed_jobs_path, alert: "Could not update job: #{e.message}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ module SolidStackWeb
2
+ module FailedJobs
3
+ class SelectionsController < ApplicationController
4
+ def create
5
+ ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
6
+ SolidQueue::FailedExecution.where(id: ids).each(&:retry)
7
+ redirect_to failed_jobs_path
8
+ rescue => e
9
+ redirect_to failed_jobs_path, alert: "Could not retry jobs: #{e.message}"
10
+ end
11
+
12
+ def destroy
13
+ ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
14
+ job_ids = SolidQueue::FailedExecution.where(id: ids).pluck(:job_id)
15
+ SolidQueue::Job.where(id: job_ids).destroy_all
16
+ redirect_to failed_jobs_path
17
+ rescue => e
18
+ redirect_to failed_jobs_path, alert: "Could not discard jobs: #{e.message}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,13 +1,29 @@
1
1
  module SolidStackWeb
2
2
  class FailedJobsController < ApplicationController
3
3
  def index
4
- scope = ::SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
5
- @pagy, @executions = pagy(scope)
4
+ respond_to do |format|
5
+ format.html do
6
+ scope = ::SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
7
+ @pagy, @executions = pagy(scope)
8
+ end
9
+ format.csv do
10
+ send_data failed_jobs_csv,
11
+ filename: "failed-jobs-#{Date.today}.csv",
12
+ type: "text/csv", disposition: "attachment"
13
+ end
14
+ end
15
+ end
16
+
17
+ def show
18
+ @execution = ::SolidQueue::FailedExecution.includes(:job).find(params[:id])
19
+ @arguments = JSON.pretty_generate(@execution.job.arguments) if @execution.job.arguments.present?
20
+ rescue JSON::GeneratorError
21
+ @arguments = @execution.job.arguments.to_s
6
22
  end
7
23
 
8
24
  def destroy
9
- execution = ::SolidQueue::FailedExecution.find(params[:id])
10
- execution.job.destroy!
25
+ @execution = ::SolidQueue::FailedExecution.find(params[:id])
26
+ @execution.job.destroy!
11
27
  @executions_remain = ::SolidQueue::FailedExecution.exists?
12
28
 
13
29
  respond_to do |format|
@@ -21,5 +37,20 @@ module SolidStackWeb
21
37
  execution.retry
22
38
  redirect_to failed_jobs_path
23
39
  end
40
+
41
+ private
42
+
43
+ def failed_jobs_csv
44
+ CSV.generate(headers: true) do |csv|
45
+ csv << %w[id class_name queue_name error_class error_message failed_at]
46
+ ::SolidQueue::FailedExecution.includes(:job).order(created_at: :desc).each do |execution|
47
+ job = execution.job
48
+ error = execution.error || {}
49
+ csv << [job.id, job.class_name, job.queue_name,
50
+ error["exception_class"], error["message"],
51
+ execution.created_at.iso8601]
52
+ end
53
+ end
54
+ end
24
55
  end
25
56
  end
@@ -0,0 +1,24 @@
1
+ module SolidStackWeb
2
+ module Jobs
3
+ class SelectionsController < ApplicationController
4
+ def destroy
5
+ status = params[:status].presence_in(Job::STATUSES) || "ready"
6
+ raise ArgumentError, "Cannot discard #{status} jobs." unless Job::DISCARDABLE.include?(status)
7
+
8
+ ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
9
+ job_ids = Job::EXECUTION_MODELS[status].where(id: ids).pluck(:job_id)
10
+ SolidQueue::Job.where(id: job_ids).destroy_all
11
+
12
+ redirect_to jobs_path(
13
+ status: status,
14
+ q: params[:q].presence,
15
+ queue: params[:queue].presence,
16
+ period: params[:period].presence_in(PERIOD_DURATIONS.keys),
17
+ priority: params[:priority].presence
18
+ )
19
+ rescue ArgumentError => e
20
+ redirect_to jobs_path(status: params[:status]), alert: e.message
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,42 +1,81 @@
1
1
  module SolidStackWeb
2
2
  class JobsController < ApplicationController
3
- EXECUTION_MODELS = {
4
- "ready" => ::SolidQueue::ReadyExecution,
5
- "scheduled" => ::SolidQueue::ScheduledExecution,
6
- "claimed" => ::SolidQueue::ClaimedExecution,
7
- "blocked" => ::SolidQueue::BlockedExecution
8
- }.freeze
9
-
10
- DISCARDABLE = %w[ready scheduled blocked].freeze
11
-
12
3
  before_action :set_status
13
- before_action :require_discardable, only: :destroy
4
+ before_action :set_filters, only: [:index, :destroy]
5
+ before_action :require_discardable, only: [:destroy]
14
6
 
15
7
  def index
16
- scope = EXECUTION_MODELS[@status].includes(:job).order(created_at: :desc)
17
- @pagy, @executions = pagy(scope)
8
+ @queue_options = Job::EXECUTION_MODELS[@status].joins(:job).distinct.pluck("solid_queue_jobs.queue_name").sort
9
+ @priority_options = Job::EXECUTION_MODELS[@status].joins(:job).distinct.pluck("solid_queue_jobs.priority").sort
10
+
11
+ respond_to do |format|
12
+ format.html { @pagy, @executions = pagy(filtered_scope) }
13
+ format.csv do
14
+ send_data jobs_csv,
15
+ filename: "jobs-#{@status}-#{Date.today}.csv",
16
+ type: "text/csv", disposition: "attachment"
17
+ end
18
+ end
19
+ end
20
+
21
+ def show
22
+ @execution = Job::EXECUTION_MODELS[@status].includes(:job).find(params[:id])
23
+ @arguments = JSON.parse(@execution.job.arguments) if @execution.job.arguments.present?
24
+ rescue JSON::ParserError
25
+ @arguments = nil
18
26
  end
19
27
 
20
28
  def destroy
21
- model = EXECUTION_MODELS[@status]
22
- execution = model.find(params[:id])
23
- execution.job.destroy!
24
- @executions_remain = model.exists?
29
+ if params[:id]
30
+ @execution = Job::EXECUTION_MODELS[@status].find(params[:id])
31
+ @execution.job.destroy!
32
+ @executions_remain = Job::EXECUTION_MODELS[@status].exists?
25
33
 
26
- respond_to do |format|
27
- format.html { redirect_to jobs_path(status: @status) }
28
- format.turbo_stream
34
+ respond_to do |format|
35
+ format.html { redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority) }
36
+ format.turbo_stream
37
+ end
38
+ else
39
+ job_ids = filtered_scope.pluck(:job_id)
40
+ SolidQueue::Job.where(id: job_ids).destroy_all
41
+ redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority)
29
42
  end
30
43
  end
31
44
 
32
45
  private
33
46
 
34
47
  def set_status
35
- @status = params[:status].presence_in(EXECUTION_MODELS.keys) || "ready"
48
+ @status = params[:status].presence_in(Job::STATUSES) || "ready"
49
+ end
50
+
51
+ def set_filters
52
+ @search = params[:q].presence
53
+ @queue = params[:queue].presence
54
+ @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
55
+ @priority = params[:priority].presence
36
56
  end
37
57
 
38
58
  def require_discardable
39
- head :unprocessable_entity unless DISCARDABLE.include?(@status)
59
+ head :unprocessable_content unless Job::DISCARDABLE.include?(@status)
60
+ end
61
+
62
+ def jobs_csv
63
+ CSV.generate(headers: true) do |csv|
64
+ csv << %w[id class_name queue_name status priority enqueued_at]
65
+ filtered_scope.each do |execution|
66
+ job = execution.job
67
+ csv << [job.id, job.class_name, job.queue_name, @status, job.priority, job.created_at.iso8601]
68
+ end
69
+ end
70
+ end
71
+
72
+ def filtered_scope
73
+ scope = Job::EXECUTION_MODELS[@status].includes(:job).order(created_at: :desc)
74
+ scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
75
+ scope = scope.references(:job).where("solid_queue_jobs.queue_name = ?", @queue) if @queue.present?
76
+ scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
77
+ scope = scope.references(:job).where("solid_queue_jobs.priority = ?", @priority.to_i) if @priority.present?
78
+ scope
40
79
  end
41
80
  end
42
81
  end
@@ -0,0 +1,6 @@
1
+ import "@hotwired/turbo"
2
+ import { Application } from "@hotwired/stimulus"
3
+ import SelectionController from "solid_stack_web/selection_controller"
4
+
5
+ const application = Application.start()
6
+ application.register("selection", SelectionController)
@@ -0,0 +1,42 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["checkbox", "selectAll", "bar", "count"]
5
+
6
+ toggle() {
7
+ this._update()
8
+ }
9
+
10
+ selectAll({ target }) {
11
+ this.checkboxTargets.forEach(cb => cb.checked = target.checked)
12
+ this._update()
13
+ }
14
+
15
+ submit({ params: { formId } }) {
16
+ const form = document.getElementById(formId)
17
+ if (!form) return
18
+ form.querySelectorAll("[data-injected-id]").forEach(el => el.remove())
19
+ this.checkboxTargets
20
+ .filter(cb => cb.checked)
21
+ .forEach(cb => {
22
+ const input = document.createElement("input")
23
+ input.type = "hidden"
24
+ input.name = "job_ids[]"
25
+ input.value = cb.value
26
+ input.dataset.injectedId = true
27
+ form.appendChild(input)
28
+ })
29
+ form.requestSubmit()
30
+ }
31
+
32
+ _update() {
33
+ const checked = this.checkboxTargets.filter(cb => cb.checked).length
34
+ const total = this.checkboxTargets.length
35
+ if (this.hasBarTarget) this.barTarget.style.display = checked > 0 ? "" : "none"
36
+ if (this.hasCountTarget) this.countTarget.textContent = checked
37
+ if (this.hasSelectAllTarget) {
38
+ this.selectAllTarget.indeterminate = checked > 0 && checked < total
39
+ this.selectAllTarget.checked = total > 0 && checked === total
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,21 @@
1
+ module SolidStackWeb
2
+ class Job
3
+ STATUSES = %w[ready scheduled claimed blocked].freeze
4
+
5
+ EXECUTION_MODELS = {
6
+ "ready" => SolidQueue::ReadyExecution,
7
+ "scheduled" => SolidQueue::ScheduledExecution,
8
+ "claimed" => SolidQueue::ClaimedExecution,
9
+ "blocked" => SolidQueue::BlockedExecution
10
+ }.freeze
11
+
12
+ DISCARDABLE = %w[ready scheduled blocked].freeze
13
+
14
+ TAB_LABELS = {
15
+ "ready" => "Ready",
16
+ "scheduled" => "Scheduled",
17
+ "claimed" => "Running",
18
+ "blocked" => "Blocked"
19
+ }.freeze
20
+ end
21
+ end
@@ -7,6 +7,7 @@
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
9
  <%= inline_styles %>
10
+ <%= javascript_importmap_tags "solid_stack_web" %>
10
11
  </head>
11
12
  <body>
12
13
  <header class="sqw-header">
@@ -0,0 +1,9 @@
1
+ <% if @executions_remain %>
2
+ <%= turbo_stream.remove "execution_#{@execution.id}" %>
3
+ <% else %>
4
+ <%= turbo_stream.replace "sqw-jobs-table" do %>
5
+ <div class="sqw-empty">
6
+ <p>No failed jobs.</p>
7
+ </div>
8
+ <% end %>
9
+ <% end %>
@@ -1,41 +1,72 @@
1
- <div class="sqw-page-header">
1
+ <div class="sqw-page-header sqw-page-header--split">
2
2
  <h1 class="sqw-page-title">Failed Jobs</h1>
3
+ <div class="sqw-header-actions">
4
+ <%= link_to "Export CSV", failed_jobs_path(format: :csv),
5
+ class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
6
+ </div>
3
7
  </div>
4
8
 
5
9
  <div id="sqw-jobs-table">
6
10
  <% if @executions.any? %>
7
- <table class="sqw-table">
8
- <thead>
9
- <tr>
10
- <th>Job Class</th>
11
- <th>Queue</th>
12
- <th>Error</th>
13
- <th>Failed At</th>
14
- <th></th>
15
- </tr>
16
- </thead>
17
- <tbody>
18
- <% @executions.each do |execution| %>
19
- <tr id="execution_<%= execution.id %>">
20
- <td class="sqw-monospace"><%= execution.job.class_name %></td>
21
- <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
22
- <td class="sqw-muted sqw-truncate"><%= execution.error&.lines&.first&.strip %></td>
23
- <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
24
- <td class="sqw-actions">
25
- <%= button_to "Retry", retry_failed_job_path(execution),
26
- method: :post, class: "sqw-btn sqw-btn--sm" %>
27
- <%= button_to "Discard", failed_job_path(execution),
28
- method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
29
- data: { turbo_confirm: "Discard this job?" } %>
30
- </td>
11
+ <div data-controller="selection">
12
+ <%= form_with url: failed_job_selection_path, method: :post, id: "retry-selection-form" do |f| %>
13
+ <% end %>
14
+
15
+ <%= form_with url: failed_job_selection_path, method: :delete, id: "discard-selection-form",
16
+ data: { turbo_confirm: "Discard selected failed jobs? This cannot be undone." } do |f| %>
17
+ <% end %>
18
+
19
+ <div class="sqw-selection-bar" data-selection-target="bar" style="display: none;">
20
+ <span class="sqw-muted"><span data-selection-target="count">0</span> selected</span>
21
+ <button type="button" class="sqw-btn sqw-btn--sm"
22
+ data-action="click->selection#submit"
23
+ data-selection-form-id-param="retry-selection-form">Retry Selected</button>
24
+ <button type="button" class="sqw-btn sqw-btn--danger sqw-btn--sm"
25
+ data-action="click->selection#submit"
26
+ data-selection-form-id-param="discard-selection-form">Discard Selected</button>
27
+ </div>
28
+
29
+ <table class="sqw-table">
30
+ <thead>
31
+ <tr>
32
+ <th><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
33
+ data-selection-target="selectAll"
34
+ data-action="change->selection#selectAll"></th>
35
+ <th>Job Class</th>
36
+ <th>Queue</th>
37
+ <th>Error</th>
38
+ <th>Failed At</th>
39
+ <th></th>
31
40
  </tr>
32
- <% end %>
33
- </tbody>
34
- </table>
35
- <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
41
+ </thead>
42
+ <tbody>
43
+ <% @executions.each do |execution| %>
44
+ <tr id="execution_<%= execution.id %>">
45
+ <td><input type="checkbox" value="<%= execution.id %>"
46
+ class="sqw-checkbox"
47
+ aria-label="Select <%= execution.job.class_name %>"
48
+ data-selection-target="checkbox"
49
+ data-action="change->selection#toggle"></td>
50
+ <td class="sqw-monospace"><%= link_to execution.job.class_name, failed_job_path(execution) %></td>
51
+ <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
52
+ <td class="sqw-muted sqw-truncate" title="<%= execution.exception_class %>: <%= execution.message %>"><%= execution.exception_class %></td>
53
+ <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
54
+ <td class="sqw-actions">
55
+ <%= button_to "Retry", retry_failed_job_path(execution),
56
+ method: :post, class: "sqw-btn sqw-btn--sm" %>
57
+ <%= button_to "Discard", failed_job_path(execution),
58
+ method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
59
+ data: { turbo_confirm: "Discard this job?" } %>
60
+ </td>
61
+ </tr>
62
+ <% end %>
63
+ </tbody>
64
+ </table>
65
+ <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
66
+ </div>
36
67
  <% else %>
37
68
  <div class="sqw-empty">
38
69
  <p>No failed jobs.</p>
39
70
  </div>
40
71
  <% end %>
41
- </div>
72
+ </div>
@@ -0,0 +1,58 @@
1
+ <div class="sqw-page-header sqw-page-header--split">
2
+ <div>
3
+ <div class="sqw-breadcrumb">
4
+ <%= link_to "Failed Jobs", failed_jobs_path %> &rsaquo; Detail
5
+ </div>
6
+ <h1 class="sqw-page-title sqw-monospace"><%= @execution.job.class_name %></h1>
7
+ </div>
8
+
9
+ <div class="sqw-detail-actions">
10
+ <%= button_to "Retry", retry_failed_job_path(@execution),
11
+ method: :post, class: "sqw-btn sqw-btn--sm" %>
12
+ <%= button_to "Discard", failed_job_path(@execution),
13
+ method: :delete, class: "sqw-btn sqw-btn--danger",
14
+ data: { turbo_confirm: "Discard this job?" } %>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="sqw-detail-grid">
19
+ <div class="sqw-detail-card sqw-detail-section">
20
+ <h2 class="sqw-section-title">Details</h2>
21
+ <dl class="sqw-dl">
22
+ <dt>Queue</dt>
23
+ <dd class="sqw-monospace"><%= @execution.job.queue_name %></dd>
24
+
25
+ <dt>Priority</dt>
26
+ <dd><%= @execution.job.priority %></dd>
27
+
28
+ <dt>Active Job ID</dt>
29
+ <dd class="sqw-monospace sqw-truncate" title="<%= @execution.job.active_job_id %>"><%= @execution.job.active_job_id.presence || "—" %></dd>
30
+
31
+ <dt>Failed At</dt>
32
+ <dd class="sqw-monospace"><%= @execution.created_at.strftime("%Y-%m-%d %H:%M:%S UTC") %></dd>
33
+
34
+ <dt>Error</dt>
35
+ <dd class="sqw-monospace"><%= @execution.exception_class %></dd>
36
+
37
+ <dt>Message</dt>
38
+ <dd class="sqw-muted"><%= @execution.message %></dd>
39
+ </dl>
40
+ </div>
41
+
42
+ <div class="sqw-detail-card sqw-detail-section">
43
+ <h2 class="sqw-section-title">Backtrace</h2>
44
+ <pre class="sqw-code-block"><%= Array(@execution.backtrace).first(10).join("\n").presence || "—" %></pre>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="sqw-detail-card sqw-detail-section" style="margin-top: 1.5rem;">
49
+ <h2 class="sqw-section-title">Arguments</h2>
50
+ <%= form_with url: failed_job_arguments_path(@execution), method: :patch do |f| %>
51
+ <%= f.text_area :arguments, value: @arguments, rows: 12,
52
+ class: "sqw-code-input", aria: { label: "Job arguments JSON" },
53
+ spellcheck: false %>
54
+ <div style="margin-top: 0.75rem;">
55
+ <%= f.submit "Update &amp; Retry".html_safe, class: "sqw-btn sqw-btn--sm" %>
56
+ </div>
57
+ <% end %>
58
+ </div>
@@ -1,7 +1,7 @@
1
1
  <% if @executions_remain %>
2
- <%= turbo_stream.remove "execution_#{params[:id]}" %>
2
+ <%= turbo_stream.remove "execution_#{@execution.id}" %>
3
3
  <% else %>
4
4
  <%= turbo_stream.replace "sqw-jobs-table" do %>
5
5
  <%= render "empty" %>
6
6
  <% end %>
7
- <% end %>
7
+ <% end %>
@@ -1,50 +1,133 @@
1
- <div class="sqw-page-header">
1
+ <div class="sqw-page-header sqw-page-header--split">
2
2
  <h1 class="sqw-page-title">Jobs</h1>
3
+ <div class="sqw-header-actions">
4
+ <%= link_to "Export CSV", jobs_path(format: :csv, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
5
+ class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
6
+ <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) && @executions&.any? %>
7
+ <%= button_to "Discard All (#{@pagy.count})",
8
+ discard_all_jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
9
+ method: :post,
10
+ class: "sqw-btn sqw-btn--danger sqw-btn--sm",
11
+ data: { turbo_confirm: "Discard all #{@pagy.count} jobs? This cannot be undone.",
12
+ turbo_frame: "_top" } %>
13
+ <% end %>
14
+ </div>
3
15
  </div>
4
16
 
5
17
  <div class="sqw-tabs">
6
- <% [["ready", "Ready"], ["scheduled", "Scheduled"], ["claimed", "Running"], ["blocked", "Blocked"]].each do |status, label| %>
7
- <%= link_to label, jobs_path(status: status),
18
+ <% SolidStackWeb::Job::TAB_LABELS.each do |status, label| %>
19
+ <%= link_to label, jobs_path(status: status, q: @search, queue: @queue, period: @period, priority: @priority),
8
20
  class: "sqw-tab #{"sqw-tab--active" if @status == status}" %>
9
21
  <% end %>
10
22
  </div>
11
23
 
24
+ <turbo-frame id="sqw-jobs-filter" data-turbo-action="advance">
25
+ <form class="sqw-filters" action="<%= jobs_path %>" method="get">
26
+ <%= hidden_field_tag :status, @status %>
27
+ <%= hidden_field_tag :period, @period %>
28
+ <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
29
+ placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class">
30
+ <% if @queue_options.size > 1 %>
31
+ <select name="queue" class="sqw-select" aria-label="Filter by queue" onchange="this.form.requestSubmit()">
32
+ <option value="">All queues</option>
33
+ <% @queue_options.each do |q| %>
34
+ <option value="<%= q %>" <%= "selected" if @queue == q %>><%= q %></option>
35
+ <% end %>
36
+ </select>
37
+ <% end %>
38
+ <% if @priority_options.size > 1 %>
39
+ <select name="priority" class="sqw-select" aria-label="Filter by priority" onchange="this.form.requestSubmit()">
40
+ <option value="">All priorities</option>
41
+ <% @priority_options.each do |p| %>
42
+ <option value="<%= p %>" <%= "selected" if @priority.to_s == p.to_s %>>Priority <%= p %></option>
43
+ <% end %>
44
+ </select>
45
+ <% end %>
46
+ <button type="submit" class="sqw-btn sqw-btn--muted sqw-btn--sm">Search</button>
47
+ <% if @search.present? || @queue.present? || @priority.present? %>
48
+ <%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
49
+ <% end %>
50
+ <div class="sqw-period-filter" role="group" aria-label="Time period">
51
+ <%= link_to "All", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority),
52
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period.nil?}" %>
53
+ <%= link_to "1h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "1h"),
54
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "1h"}" %>
55
+ <%= link_to "24h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "24h"),
56
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "24h"}" %>
57
+ <%= link_to "7d", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "7d"),
58
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "7d"}" %>
59
+ </div>
60
+ </form>
61
+
12
62
  <div id="sqw-jobs-table">
13
63
  <% if @executions.any? %>
14
- <table class="sqw-table">
15
- <thead>
16
- <tr>
17
- <th>Job Class</th>
18
- <th>Queue</th>
19
- <th>Priority</th>
20
- <th>Enqueued At</th>
21
- <% if @status == "scheduled" %><th>Scheduled At</th><% end %>
22
- <th></th>
23
- </tr>
24
- </thead>
25
- <tbody>
26
- <% @executions.each do |execution| %>
27
- <tr id="execution_<%= execution.id %>">
28
- <td class="sqw-monospace"><%= execution.job.class_name %></td>
29
- <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
30
- <td><%= execution.job.priority %></td>
31
- <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
32
- <% if @status == "scheduled" %>
33
- <td class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
64
+ <div data-controller="<%= "selection" if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>">
65
+ <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
66
+ <%= form_with url: job_selection_path, method: :delete, id: "job-selection-form",
67
+ data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do |f| %>
68
+ <%= f.hidden_field :status, value: @status %>
69
+ <%= f.hidden_field :q, value: @search %>
70
+ <%= f.hidden_field :queue, value: @queue %>
71
+ <%= f.hidden_field :period, value: @period %>
72
+ <%= f.hidden_field :priority, value: @priority %>
73
+ <% end %>
74
+
75
+ <div class="sqw-selection-bar" data-selection-target="bar" style="display: none;">
76
+ <span class="sqw-muted"><span data-selection-target="count">0</span> selected</span>
77
+ <button type="button" class="sqw-btn sqw-btn--danger sqw-btn--sm"
78
+ data-action="click->selection#submit"
79
+ data-selection-form-id-param="job-selection-form">Discard Selected</button>
80
+ </div>
81
+ <% end %>
82
+
83
+ <table class="sqw-table">
84
+ <thead>
85
+ <tr>
86
+ <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
87
+ <th><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
88
+ data-selection-target="selectAll"
89
+ data-action="change->selection#selectAll"></th>
34
90
  <% end %>
35
- <td class="sqw-actions">
36
- <% if %w[ready scheduled blocked].include?(@status) %>
37
- <%= button_to "Discard", job_path(execution, status: @status),
38
- method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
39
- data: { turbo_confirm: "Discard this job?" } %>
40
- <% end %>
41
- </td>
91
+ <th>Job Class</th>
92
+ <th>Queue</th>
93
+ <th>Priority</th>
94
+ <th>Enqueued At</th>
95
+ <% if @status == "scheduled" %><th>Scheduled At</th><% end %>
96
+ <th></th>
42
97
  </tr>
43
- <% end %>
44
- </tbody>
45
- </table>
46
- <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
98
+ </thead>
99
+ <tbody>
100
+ <% @executions.each do |execution| %>
101
+ <tr id="execution_<%= execution.id %>">
102
+ <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
103
+ <td><input type="checkbox" value="<%= execution.id %>"
104
+ class="sqw-checkbox"
105
+ aria-label="Select <%= execution.job.class_name %>"
106
+ data-selection-target="checkbox"
107
+ data-action="change->selection#toggle"></td>
108
+ <% end %>
109
+ <td class="sqw-monospace"><%= link_to execution.job.class_name, job_path(execution.id, status: @status), data: { turbo_frame: "_top" } %></td>
110
+ <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
111
+ <td><%= execution.job.priority %></td>
112
+ <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
113
+ <% if @status == "scheduled" %>
114
+ <td class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
115
+ <% end %>
116
+ <td class="sqw-actions">
117
+ <% if %w[ready scheduled blocked].include?(@status) %>
118
+ <%= button_to "Discard", job_path(execution, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
119
+ method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
120
+ data: { turbo_confirm: "Discard this job?" } %>
121
+ <% end %>
122
+ </td>
123
+ </tr>
124
+ <% end %>
125
+ </tbody>
126
+ </table>
127
+ <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
128
+ </div>
47
129
  <% else %>
48
130
  <%= render "empty" %>
49
131
  <% end %>
50
132
  </div>
133
+ </turbo-frame>
@@ -0,0 +1,57 @@
1
+ <div class="sqw-page-header sqw-page-header--split">
2
+ <div>
3
+ <div class="sqw-breadcrumb">
4
+ <%= link_to "Jobs", jobs_path(status: params[:status]) %> &rsaquo; Detail
5
+ </div>
6
+ <h1 class="sqw-page-title sqw-monospace"><%= @execution.job.class_name %></h1>
7
+ </div>
8
+
9
+ <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
10
+ <div class="sqw-detail-actions">
11
+ <%= button_to "Discard Job", job_path(@execution.id, status: @status),
12
+ method: :delete, class: "sqw-btn sqw-btn--danger",
13
+ data: { turbo_confirm: "Discard this job?" } %>
14
+ </div>
15
+ <% end %>
16
+ </div>
17
+
18
+ <div class="sqw-detail-grid">
19
+ <div class="sqw-detail-card sqw-detail-section">
20
+ <h2 class="sqw-section-title">Details</h2>
21
+ <dl class="sqw-dl">
22
+ <dt>Status</dt>
23
+ <dd><span class="sqw-badge sqw-badge--<%= @status %>"><%= SolidStackWeb::Job::TAB_LABELS[@status] %></span></dd>
24
+
25
+ <dt>Queue</dt>
26
+ <dd class="sqw-monospace"><%= @execution.job.queue_name %></dd>
27
+
28
+ <dt>Priority</dt>
29
+ <dd><%= @execution.job.priority %></dd>
30
+
31
+ <dt>Active Job ID</dt>
32
+ <dd class="sqw-monospace sqw-truncate" title="<%= @execution.job.active_job_id %>"><%= @execution.job.active_job_id.presence || "—" %></dd>
33
+
34
+ <dt>Concurrency Key</dt>
35
+ <dd class="sqw-monospace"><%= @execution.job.concurrency_key.presence || "—" %></dd>
36
+
37
+ <% if @status == "blocked" %>
38
+ <dt>Blocked Until</dt>
39
+ <dd class="sqw-monospace"><%= @execution.expires_at ? @execution.expires_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %></dd>
40
+ <% end %>
41
+
42
+ <dt>Enqueued At</dt>
43
+ <dd class="sqw-monospace"><%= @execution.job.created_at.strftime("%Y-%m-%d %H:%M:%S UTC") %></dd>
44
+
45
+ <dt>Scheduled At</dt>
46
+ <dd class="sqw-monospace"><%= @execution.job.scheduled_at ? @execution.job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %></dd>
47
+
48
+ <dt>Finished At</dt>
49
+ <dd class="sqw-monospace"><%= @execution.job.finished_at ? @execution.job.finished_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %></dd>
50
+ </dl>
51
+ </div>
52
+
53
+ <div class="sqw-detail-card sqw-detail-section">
54
+ <h2 class="sqw-section-title">Arguments</h2>
55
+ <pre class="sqw-code-block"><%= @arguments ? JSON.pretty_generate(@arguments) : (@execution.job.arguments || "—") %></pre>
56
+ </div>
57
+ </div>
@@ -0,0 +1,2 @@
1
+ pin "solid_stack_web", to: "solid_stack_web/application.js"
2
+ pin "solid_stack_web/selection_controller", to: "solid_stack_web/selection_controller.js"
data/config/routes.rb CHANGED
@@ -1,10 +1,18 @@
1
1
  SolidStackWeb::Engine.routes.draw do
2
2
  root to: "dashboard#index"
3
3
 
4
- resources :jobs, only: [:index, :destroy]
4
+ resource :job_selection, path: "jobs/selection", only: [:destroy], controller: "jobs/selections"
5
+ resource :failed_job_selection, path: "failed_jobs/selection", only: [:create, :destroy], controller: "failed_jobs/selections"
5
6
 
6
- resources :failed_jobs, only: [:index, :destroy] do
7
+ resources :jobs, only: [:index, :show, :destroy] do
8
+ collection do
9
+ post :discard_all, action: :destroy
10
+ end
11
+ end
12
+
13
+ resources :failed_jobs, only: [:index, :show, :destroy] do
7
14
  member { post :retry }
15
+ resource :arguments, only: [:update], controller: "failed_jobs/arguments"
8
16
  end
9
17
 
10
18
  resources :queues, only: [:index] do
@@ -3,6 +3,8 @@ require "pagy/toolbox/paginators/method"
3
3
  require "solid_queue"
4
4
  require "solid_cache"
5
5
  require "solid_cable"
6
+ require "turbo-rails"
7
+ require "importmap-rails"
6
8
 
7
9
  module SolidStackWeb
8
10
  class Engine < ::Rails::Engine
@@ -10,6 +12,19 @@ module SolidStackWeb
10
12
 
11
13
  config.i18n.load_path += Gem.find_files("pagy/locales/en.yml")
12
14
 
15
+ initializer "solid_stack_web.assets" do |app|
16
+ if app.config.respond_to?(:assets)
17
+ app.config.assets.paths << root.join("app/javascript")
18
+ end
19
+ end
20
+
21
+ initializer "solid_stack_web.importmap", before: "importmap" do |app|
22
+ if app.config.respond_to?(:importmap)
23
+ app.config.importmap.paths << root.join("config/importmap.rb")
24
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
25
+ end
26
+ end
27
+
13
28
  initializer "solid_stack_web.pagy" do |app|
14
29
  app.config.after_initialize do
15
30
  Pagy::OPTIONS[:limit] = SolidStackWeb.page_size
@@ -1,3 +1,3 @@
1
1
  module SolidStackWeb
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_stack_web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -79,6 +79,48 @@ dependencies:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '1.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: turbo-rails
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '2.0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '2.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: importmap-rails
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '1.2'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '1.2'
110
+ - !ruby/object:Gem::Dependency
111
+ name: csv
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '3.0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '3.0'
82
124
  description: Mount SolidStackWeb in any Rails app using the Solid Stack to get a single
83
125
  dashboard covering Solid Queue job monitoring, Solid Cache statistics, and Solid
84
126
  Cable connection observability — all without leaving your app.
@@ -98,26 +140,38 @@ files:
98
140
  - app/assets/stylesheets/solid_stack_web/_05_badges.css
99
141
  - app/assets/stylesheets/solid_stack_web/_06_buttons.css
100
142
  - app/assets/stylesheets/solid_stack_web/_07_dashboard.css
143
+ - app/assets/stylesheets/solid_stack_web/_08_filters.css
144
+ - app/assets/stylesheets/solid_stack_web/_09_detail.css
101
145
  - app/assets/stylesheets/solid_stack_web/application.css
102
146
  - app/controllers/solid_stack_web/application_controller.rb
103
147
  - app/controllers/solid_stack_web/cable_controller.rb
104
148
  - app/controllers/solid_stack_web/cache_controller.rb
105
149
  - app/controllers/solid_stack_web/dashboard_controller.rb
150
+ - app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb
151
+ - app/controllers/solid_stack_web/failed_jobs/selections_controller.rb
106
152
  - app/controllers/solid_stack_web/failed_jobs_controller.rb
153
+ - app/controllers/solid_stack_web/jobs/selections_controller.rb
107
154
  - app/controllers/solid_stack_web/jobs_controller.rb
108
155
  - app/controllers/solid_stack_web/processes_controller.rb
109
156
  - app/controllers/solid_stack_web/queues_controller.rb
110
157
  - app/helpers/solid_stack_web/application_helper.rb
158
+ - app/javascript/solid_stack_web/application.js
159
+ - app/javascript/solid_stack_web/selection_controller.js
160
+ - app/models/solid_stack_web/job.rb
111
161
  - app/views/layouts/solid_stack_web/application.html.erb
112
162
  - app/views/solid_stack_web/cable/index.html.erb
113
163
  - app/views/solid_stack_web/cache/index.html.erb
114
164
  - app/views/solid_stack_web/dashboard/index.html.erb
165
+ - app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb
115
166
  - app/views/solid_stack_web/failed_jobs/index.html.erb
167
+ - app/views/solid_stack_web/failed_jobs/show.html.erb
116
168
  - app/views/solid_stack_web/jobs/_empty.html.erb
117
169
  - app/views/solid_stack_web/jobs/destroy.turbo_stream.erb
118
170
  - app/views/solid_stack_web/jobs/index.html.erb
171
+ - app/views/solid_stack_web/jobs/show.html.erb
119
172
  - app/views/solid_stack_web/processes/index.html.erb
120
173
  - app/views/solid_stack_web/queues/index.html.erb
174
+ - config/importmap.rb
121
175
  - config/routes.rb
122
176
  - lib/solid_stack_web.rb
123
177
  - lib/solid_stack_web/engine.rb