diff --git a/.gitignore b/.gitignore index edc6828..a11dbc4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ /spec/dummy/log/*.log /spec/dummy/storage/ /spec/dummy/tmp/ + +coverage/ diff --git a/.rubocop.yml b/.rubocop.yml index e518bc1..4c43e84 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,6 +6,7 @@ AllCops: - "bin/release" - "README.md" - "spec/dummy/config/database.yml" + - "spec/dummy/db/seeds.rb" Layout/SpaceInsideArrayLiteralBrackets: Enabled: true diff --git a/CHANGELOG.md b/CHANGELOG.md index a97ca36..6e89245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,4 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -[Unreleased]: https://github.com/eclectic-coding/solid_stack_web/compare/HEAD +## [0.1.0] - 2026-05-24 + +### Added + +- Overview dashboard with live stats across Solid Queue, Solid Cache, and Solid Cable +- Solid Queue job monitoring: filterable list by status (ready, scheduled, claimed, blocked) with paginated results +- Solid Queue failed jobs: dedicated view with per-job retry and discard actions +- Queue management: pause and resume individual queues +- Worker process list showing all registered Solid Queue processes +- Solid Cache monitoring: entry count and total byte size +- Solid Cable monitoring: message count and active channel count +- Turbo Stream job discard: removes the row inline, or replaces the table with an empty state when the last job is discarded +- Authentication hook — configure via `SolidQueueWeb.authenticate { |controller| ... }` in an initializer; falls back to HTTP Basic if the block returns falsy +- Configurable page size via `SolidQueueWeb.page_size` (default: 25) +- Inline CSS delivery — no asset pipeline dependency, safe to mount in any host app +- Two-tier contextual navigation per section (Queue / Cache / Cable) +- No runtime JavaScript dependency — all interactions use standard form POSTs or Turbo Stream + +[Unreleased]: https://github.com/eclectic-coding/solid_stack_web/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/eclectic-coding/solid_stack_web/releases/tag/v0.1.0 diff --git a/README.md b/README.md index ecd1751..8e332ef 100644 --- a/README.md +++ b/README.md @@ -5,30 +5,78 @@ [](https://www.ruby-lang.org) [](https://codecov.io/gh/eclectic-coding/solid_stack_web) -Short description and motivation. +A mountable Rails engine that provides a unified web dashboard for the full [Solid Stack](https://github.com/rails/solid_queue) — **Solid Queue**, **Solid Cache**, and **Solid Cable** — in a single interface with no asset pipeline dependency and no JavaScript runtime requirement. -## Usage -How to use my plugin. +## Features + +- **Overview dashboard** with live counts across all three Solid Stack components +- **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked), manage failed jobs (retry / discard), pause/resume queues, and inspect worker processes +- **Solid Cache** — entry count and total byte size at a glance +- **Solid Cable** — active message count and distinct channel count +- **Turbo Stream** job discard — removes the row inline without a full page reload +- **Authentication hook** — plug in your own auth logic (Devise, Basic Auth, custom) via a one-line initializer +- **Zero asset pipeline coupling** — CSS is injected inline; safe to mount in any host app ## Installation -Add this line to your application's Gemfile: + +Add the gem to your application's `Gemfile`: ```ruby gem "solid_stack_web" ``` -And then execute: +Run: + ```bash -$ bundle +bundle install ``` -Or install it yourself as: -```bash -$ gem install solid_stack_web +Mount the engine in `config/routes.rb`: + +```ruby +mount SolidStackWeb::Engine, at: "/solid_stack" +``` + +The dashboard will be available at `/solid_stack` (or whatever path you choose). + +## Configuration + +Create an initializer at `config/initializers/solid_stack_web.rb`: + +```ruby +SolidStackWeb.configure do |config| + # Number of items per paginated page (default: 25) + config.page_size = 50 + + # Authentication — block runs in controller context. + # Return a truthy value to allow access; falsy falls back to HTTP Basic. + config.authenticate do + current_user&.admin? + end +end ``` +### Authentication + +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. + +## Requirements + +- Ruby >= 3.3 +- Rails >= 8.1.3 +- [solid_queue](https://github.com/rails/solid_queue) >= 1.0 +- [solid_cache](https://github.com/rails/solid_cache) >= 1.0 +- [solid_cable](https://github.com/rails/solid_cable) >= 1.0 + ## Contributing -Contribution directions go here. + +1. Fork the repository +2. Create a feature branch (`git checkout -b feat/my-feature`) +3. Run the test suite: `bundle exec rake` +4. Open a pull request + +Bug reports and feature requests are welcome on [GitHub Issues](https://github.com/eclectic-coding/solid_stack_web/issues). ## License -The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). + +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). \ No newline at end of file diff --git a/app/assets/stylesheets/solid_stack_web/_01_base.css b/app/assets/stylesheets/solid_stack_web/_01_base.css new file mode 100644 index 0000000..d423632 --- /dev/null +++ b/app/assets/stylesheets/solid_stack_web/_01_base.css @@ -0,0 +1,32 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --bg: #f8f9fa; + --surface: #ffffff; + --border: #dee2e6; + --text: #212529; + --muted: #6c757d; + --primary: #0d6efd; + --danger: #dc3545; + --warning: #fd7e14; + --success: #198754; + --info: #0dcaf0; + --purple: #6f42c1; + --radius: 6px; + --shadow: 0 1px 3px rgba(0,0,0,.08); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 14px; + line-height: 1.5; + color: var(--text); + background: var(--bg); +} + +a { color: var(--primary); text-decoration: none; } +a:hover { text-decoration: underline; } + +.sqw-monospace { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 13px; } +.sqw-muted { color: var(--muted); } +.sqw-truncate { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } diff --git a/app/assets/stylesheets/solid_stack_web/_02_layout.css b/app/assets/stylesheets/solid_stack_web/_02_layout.css new file mode 100644 index 0000000..b1a77ec --- /dev/null +++ b/app/assets/stylesheets/solid_stack_web/_02_layout.css @@ -0,0 +1,83 @@ +.sqw-header { + background: var(--surface); + border-bottom: 1px solid var(--border); + box-shadow: var(--shadow); + position: sticky; + top: 0; + z-index: 100; +} + +.sqw-header__inner { + display: flex; + align-items: center; + gap: 2rem; + max-width: 1200px; + margin: 0 auto; + padding: 0 1.5rem; + height: 52px; +} + +.sqw-header__logo { + font-weight: 700; + font-size: 16px; + color: var(--text); + white-space: nowrap; +} +.sqw-header__logo:hover { text-decoration: none; color: var(--primary); } + +.sqw-nav { display: flex; gap: 0.25rem; } + +.sqw-nav__link { + padding: 0.35rem 0.75rem; + border-radius: var(--radius); + color: var(--muted); + font-size: 13px; + font-weight: 500; + transition: background 0.1s, color 0.1s; +} +.sqw-nav__link:hover { background: var(--bg); color: var(--text); text-decoration: none; } +.sqw-nav__link--active { background: var(--bg); color: var(--text); } + +.sqw-subnav { + background: var(--bg); + border-bottom: 1px solid var(--border); +} + +.sqw-subnav__inner { + display: flex; + align-items: center; + gap: 0.125rem; + max-width: 1200px; + margin: 0 auto; + padding: 0 1.5rem; + height: 36px; +} + +.sqw-subnav__link { + padding: 0.2rem 0.625rem; + border-radius: var(--radius); + color: var(--muted); + font-size: 12px; + font-weight: 500; + transition: background 0.1s, color 0.1s; +} +.sqw-subnav__link:hover { background: var(--surface); color: var(--text); text-decoration: none; } +.sqw-subnav__link--active { background: var(--surface); color: var(--text); } + +.sqw-main { + max-width: 1200px; + margin: 0 auto; + padding: 1.5rem; +} + +.sqw-page-header { margin-bottom: 1.25rem; } +.sqw-page-title { font-size: 20px; font-weight: 600; } + +.sqw-flash { + padding: 0.75rem 1rem; + border-radius: var(--radius); + margin-bottom: 1rem; + font-size: 13px; +} +.sqw-flash--notice { background: #d1e7dd; color: #0a3622; border: 1px solid #a3cfbb; } +.sqw-flash--alert { background: #f8d7da; color: #58151c; border: 1px solid #f1aeb5; } diff --git a/app/assets/stylesheets/solid_stack_web/_03_stats.css b/app/assets/stylesheets/solid_stack_web/_03_stats.css new file mode 100644 index 0000000..48b5616 --- /dev/null +++ b/app/assets/stylesheets/solid_stack_web/_03_stats.css @@ -0,0 +1,31 @@ +.sqw-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.sqw-stat { + display: flex; + flex-direction: column; + gap: 0.5rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem 1.25rem; + box-shadow: var(--shadow); + color: var(--text); + transition: box-shadow 0.15s; +} +a.sqw-stat:hover { box-shadow: 0 3px 8px rgba(0,0,0,.12); text-decoration: none; } + +.sqw-stat__label { font-size: 12px; font-weight: 500; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; } +.sqw-stat__value { font-size: 28px; font-weight: 700; line-height: 1; } + +.sqw-stat--ready .sqw-stat__value { color: var(--success); } +.sqw-stat--scheduled .sqw-stat__value { color: var(--info); } +.sqw-stat--claimed .sqw-stat__value { color: var(--primary); } +.sqw-stat--blocked .sqw-stat__value { color: var(--warning); } +.sqw-stat--failed .sqw-stat__value { color: var(--danger); } +.sqw-stat--cache .sqw-stat__value { color: var(--purple); } +.sqw-stat--cable .sqw-stat__value { color: var(--info); } diff --git a/app/assets/stylesheets/solid_stack_web/_04_table.css b/app/assets/stylesheets/solid_stack_web/_04_table.css new file mode 100644 index 0000000..bea430e --- /dev/null +++ b/app/assets/stylesheets/solid_stack_web/_04_table.css @@ -0,0 +1,39 @@ +.sqw-table { + width: 100%; + border-collapse: collapse; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; +} + +.sqw-table th, +.sqw-table td { + padding: 0.6rem 0.875rem; + text-align: left; + border-bottom: 1px solid var(--border); +} + +.sqw-table th { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--muted); + background: var(--bg); +} + +.sqw-table tbody tr:last-child td { border-bottom: none; } +.sqw-table tbody tr:hover { background: #f9fafb; } + +.sqw-actions { text-align: right; white-space: nowrap; } + +.sqw-empty { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 3rem 1.5rem; + text-align: center; + color: var(--muted); +} diff --git a/app/assets/stylesheets/solid_stack_web/_05_badges.css b/app/assets/stylesheets/solid_stack_web/_05_badges.css new file mode 100644 index 0000000..91b0f9b --- /dev/null +++ b/app/assets/stylesheets/solid_stack_web/_05_badges.css @@ -0,0 +1,20 @@ +.sqw-badge { + display: inline-block; + padding: 0.2em 0.55em; + font-size: 11px; + font-weight: 600; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: .04em; +} + +.sqw-badge--ready { background: #d1e7dd; color: #0a3622; } +.sqw-badge--scheduled { background: #cff4fc; color: #055160; } +.sqw-badge--claimed { background: #cfe2ff; color: #084298; } +.sqw-badge--blocked { background: #fff3cd; color: #664d03; } +.sqw-badge--failed { background: #f8d7da; color: #58151c; } +.sqw-badge--paused { background: #e2e3e5; color: #41464b; } +.sqw-badge--queue { background: #e9ecef; color: #495057; font-weight: 500; text-transform: none; letter-spacing: 0; } +.sqw-badge--worker { background: #cfe2ff; color: #084298; } +.sqw-badge--supervisor { background: #d1e7dd; color: #0a3622; } +.sqw-badge--dispatcher { background: #fff3cd; color: #664d03; } diff --git a/app/assets/stylesheets/solid_stack_web/_06_buttons.css b/app/assets/stylesheets/solid_stack_web/_06_buttons.css new file mode 100644 index 0000000..8156af4 --- /dev/null +++ b/app/assets/stylesheets/solid_stack_web/_06_buttons.css @@ -0,0 +1,40 @@ +.sqw-btn { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.4rem 0.875rem; + font-size: 13px; + font-weight: 500; + border: 1px solid transparent; + border-radius: var(--radius); + cursor: pointer; + background: var(--primary); + color: #fff; + transition: opacity 0.1s; +} +.sqw-btn:hover { opacity: 0.88; text-decoration: none; } + +.sqw-btn--sm { padding: 0.25rem 0.6rem; font-size: 12px; } +.sqw-btn--danger { background: var(--danger); } +.sqw-btn--muted { background: var(--surface); color: var(--text); border-color: var(--border); } +.sqw-btn--muted:hover { background: var(--bg); } + +.sqw-tabs { + display: flex; + gap: 0.25rem; + margin-bottom: 1rem; + border-bottom: 1px solid var(--border); + padding-bottom: 0; +} + +.sqw-tab { + padding: 0.5rem 1rem; + font-size: 13px; + font-weight: 500; + color: var(--muted); + border-bottom: 2px solid transparent; + margin-bottom: -1px; + transition: color 0.1s, border-color 0.1s; +} +.sqw-tab:hover { color: var(--text); text-decoration: none; } +.sqw-tab--active { color: var(--primary); border-bottom-color: var(--primary); } diff --git a/app/assets/stylesheets/solid_stack_web/_07_dashboard.css b/app/assets/stylesheets/solid_stack_web/_07_dashboard.css new file mode 100644 index 0000000..98c9766 --- /dev/null +++ b/app/assets/stylesheets/solid_stack_web/_07_dashboard.css @@ -0,0 +1,77 @@ +.sqw-gem-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1.5rem; + margin-bottom: 1.5rem; +} + +.sqw-gem-card { + background: var(--surface); + border: 1px solid var(--border); + border-top: 3px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; +} + +.sqw-gem-card--queue { border-top-color: var(--primary); } +.sqw-gem-card--cache { border-top-color: var(--purple); } +.sqw-gem-card--cable { border-top-color: var(--info); } + +.sqw-gem-card__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.875rem 1.25rem; + border-bottom: 1px solid var(--border); +} + +.sqw-gem-card__title { + font-size: 13px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .06em; + color: var(--text); +} + +.sqw-gem-card__link { + font-size: 12px; + font-weight: 500; + color: var(--muted); +} +.sqw-gem-card__link:hover { color: var(--primary); text-decoration: none; } + +.sqw-gem-card__body { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); + gap: 1.25rem; + padding: 1.25rem; +} + +.sqw-inline-stat { + display: flex; + flex-direction: column; + gap: 0.25rem; + color: var(--text); + transition: opacity 0.15s; +} +a.sqw-inline-stat:hover { opacity: 0.7; text-decoration: none; } + +.sqw-inline-stat__label { + font-size: 11px; + font-weight: 500; + color: var(--muted); + text-transform: uppercase; + letter-spacing: .04em; +} + +.sqw-inline-stat__value { font-size: 28px; font-weight: 700; line-height: 1; } + +.sqw-inline-stat--ready .sqw-inline-stat__value { color: var(--success); } +.sqw-inline-stat--scheduled .sqw-inline-stat__value { color: var(--info); } +.sqw-inline-stat--claimed .sqw-inline-stat__value { color: var(--primary); } +.sqw-inline-stat--blocked .sqw-inline-stat__value { color: var(--warning); } +.sqw-inline-stat--failed .sqw-inline-stat__value { color: var(--danger); } +.sqw-inline-stat--cache .sqw-inline-stat__value { color: var(--purple); } +.sqw-inline-stat--cable .sqw-inline-stat__value { color: var(--info); } +.sqw-inline-stat--neutral .sqw-inline-stat__value { color: var(--muted); } \ No newline at end of file diff --git a/app/assets/stylesheets/solid_stack_web/application.css b/app/assets/stylesheets/solid_stack_web/application.css index 0ebd7fe..537a451 100644 --- a/app/assets/stylesheets/solid_stack_web/application.css +++ b/app/assets/stylesheets/solid_stack_web/application.css @@ -1,15 +1 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . - *= require_self - */ +/* Styles are loaded inline via the inline_styles helper which globs _*.css files. */ diff --git a/app/controllers/solid_stack_web/application_controller.rb b/app/controllers/solid_stack_web/application_controller.rb index 60a524f..2e4b5c4 100644 --- a/app/controllers/solid_stack_web/application_controller.rb +++ b/app/controllers/solid_stack_web/application_controller.rb @@ -5,8 +5,19 @@ class ApplicationController < ActionController::Base before_action :authenticate! around_action :with_database_connection + helper_method :current_section + private + def current_section + case controller_name + when "jobs", "failed_jobs", "queues", "processes" then :queue + when "cache" then :cache + when "cable" then :cable + else :overview + end + end + def with_database_connection config = SolidStackWeb.connects_to return yield unless config diff --git a/app/controllers/solid_stack_web/cable_controller.rb b/app/controllers/solid_stack_web/cable_controller.rb new file mode 100644 index 0000000..722f362 --- /dev/null +++ b/app/controllers/solid_stack_web/cable_controller.rb @@ -0,0 +1,8 @@ +module SolidStackWeb + class CableController < ApplicationController + def index + @total_messages = ::SolidCable::Message.count + @channels = ::SolidCable::Message.distinct.pluck(:channel).sort + end + end +end diff --git a/app/controllers/solid_stack_web/cache_controller.rb b/app/controllers/solid_stack_web/cache_controller.rb new file mode 100644 index 0000000..00d2fa8 --- /dev/null +++ b/app/controllers/solid_stack_web/cache_controller.rb @@ -0,0 +1,8 @@ +module SolidStackWeb + class CacheController < ApplicationController + def index + @total_entries = ::SolidCache::Entry.count + @total_byte_size = ::SolidCache::Entry.sum(:byte_size) + end + end +end diff --git a/app/controllers/solid_stack_web/dashboard_controller.rb b/app/controllers/solid_stack_web/dashboard_controller.rb new file mode 100644 index 0000000..81c4800 --- /dev/null +++ b/app/controllers/solid_stack_web/dashboard_controller.rb @@ -0,0 +1,22 @@ +module SolidStackWeb + class DashboardController < ApplicationController + def index + @queue_stats = { + ready: ::SolidQueue::ReadyExecution.count, + scheduled: ::SolidQueue::ScheduledExecution.count, + claimed: ::SolidQueue::ClaimedExecution.count, + blocked: ::SolidQueue::BlockedExecution.count, + failed: ::SolidQueue::FailedExecution.count, + processes: ::SolidQueue::Process.count + } + @cache_stats = { + entries: ::SolidCache::Entry.count, + byte_size: ::SolidCache::Entry.sum(:byte_size) + } + @cable_stats = { + messages: ::SolidCable::Message.count, + channels: ::SolidCable::Message.distinct.count(:channel) + } + end + end +end diff --git a/app/controllers/solid_stack_web/failed_jobs_controller.rb b/app/controllers/solid_stack_web/failed_jobs_controller.rb new file mode 100644 index 0000000..ac8da23 --- /dev/null +++ b/app/controllers/solid_stack_web/failed_jobs_controller.rb @@ -0,0 +1,25 @@ +module SolidStackWeb + class FailedJobsController < ApplicationController + def index + scope = ::SolidQueue::FailedExecution.includes(:job).order(created_at: :desc) + @pagy, @executions = pagy(scope) + end + + def destroy + execution = ::SolidQueue::FailedExecution.find(params[:id]) + execution.job.destroy! + @executions_remain = ::SolidQueue::FailedExecution.exists? + + respond_to do |format| + format.html { redirect_to failed_jobs_path } + format.turbo_stream + end + end + + def retry + execution = ::SolidQueue::FailedExecution.find(params[:id]) + execution.retry + redirect_to failed_jobs_path + end + end +end diff --git a/app/controllers/solid_stack_web/jobs_controller.rb b/app/controllers/solid_stack_web/jobs_controller.rb new file mode 100644 index 0000000..3cb4bea --- /dev/null +++ b/app/controllers/solid_stack_web/jobs_controller.rb @@ -0,0 +1,42 @@ +module SolidStackWeb + class JobsController < ApplicationController + EXECUTION_MODELS = { + "ready" => ::SolidQueue::ReadyExecution, + "scheduled" => ::SolidQueue::ScheduledExecution, + "claimed" => ::SolidQueue::ClaimedExecution, + "blocked" => ::SolidQueue::BlockedExecution + }.freeze + + DISCARDABLE = %w[ready scheduled blocked].freeze + + before_action :set_status + before_action :require_discardable, only: :destroy + + def index + scope = EXECUTION_MODELS[@status].includes(:job).order(created_at: :desc) + @pagy, @executions = pagy(scope) + end + + def destroy + model = EXECUTION_MODELS[@status] + execution = model.find(params[:id]) + execution.job.destroy! + @executions_remain = model.exists? + + respond_to do |format| + format.html { redirect_to jobs_path(status: @status) } + format.turbo_stream + end + end + + private + + def set_status + @status = params[:status].presence_in(EXECUTION_MODELS.keys) || "ready" + end + + def require_discardable + head :unprocessable_entity unless DISCARDABLE.include?(@status) + end + end +end diff --git a/app/controllers/solid_stack_web/processes_controller.rb b/app/controllers/solid_stack_web/processes_controller.rb new file mode 100644 index 0000000..cbe202d --- /dev/null +++ b/app/controllers/solid_stack_web/processes_controller.rb @@ -0,0 +1,7 @@ +module SolidStackWeb + class ProcessesController < ApplicationController + def index + @processes = ::SolidQueue::Process.order(:kind, :name) + end + end +end diff --git a/app/controllers/solid_stack_web/queues_controller.rb b/app/controllers/solid_stack_web/queues_controller.rb new file mode 100644 index 0000000..4f2a582 --- /dev/null +++ b/app/controllers/solid_stack_web/queues_controller.rb @@ -0,0 +1,26 @@ +module SolidStackWeb + class QueuesController < ApplicationController + def index + queue_names = ::SolidQueue::ReadyExecution.distinct.pluck(:queue_name) + paused = ::SolidQueue::Pause.pluck(:queue_name).to_set + + @queues = queue_names.sort.map do |name| + { + name: name, + size: ::SolidQueue::ReadyExecution.where(queue_name: name).count, + paused: paused.include?(name) + } + end + end + + def pause + ::SolidQueue::Pause.find_or_create_by!(queue_name: params[:id]) + redirect_to queues_path + end + + def resume + ::SolidQueue::Pause.find_by(queue_name: params[:id])&.destroy + redirect_to queues_path + end + end +end diff --git a/app/views/layouts/solid_stack_web/application.html.erb b/app/views/layouts/solid_stack_web/application.html.erb index 071f681..7b9b998 100644 --- a/app/views/layouts/solid_stack_web/application.html.erb +++ b/app/views/layouts/solid_stack_web/application.html.erb @@ -9,15 +9,39 @@ <%= inline_styles %>
-