diff --git a/app/controllers/mission_control/jobs/dashboard_controller.rb b/app/controllers/mission_control/jobs/dashboard_controller.rb
new file mode 100644
index 00000000..238bd9c2
--- /dev/null
+++ b/app/controllers/mission_control/jobs/dashboard_controller.rb
@@ -0,0 +1,4 @@
+class MissionControl::Jobs::DashboardController < MissionControl::Jobs::ApplicationController
+ def index
+ end
+end
diff --git a/app/controllers/mission_control/jobs/internal_api/dashboard_controller.rb b/app/controllers/mission_control/jobs/internal_api/dashboard_controller.rb
new file mode 100644
index 00000000..ad626003
--- /dev/null
+++ b/app/controllers/mission_control/jobs/internal_api/dashboard_controller.rb
@@ -0,0 +1,35 @@
+class MissionControl::Jobs::InternalApi::DashboardController < MissionControl::Jobs.base_controller_class.constantize
+ include ActionView::Helpers::NumberHelper
+
+ def index
+ render json: {
+ uptime: {
+ label: Time.now.strftime("%H:%M:%S"),
+ pending: queue_job.pendings.where.not(id: failed_execution.select(:job_id)).size,
+ failed: failed_execution.where("created_at >= ?", time_to_consult.seconds.ago).size,
+ finished: queue_job.finisheds.where("finished_at >= ?", time_to_consult.seconds.ago).size,
+ },
+ total: {
+ failed: number_with_delimiter(ActiveJob.jobs.failed.count),
+ pending: number_with_delimiter(ActiveJob.jobs.pending.count),
+ scheduled: number_with_delimiter(ActiveJob.jobs.scheduled.count),
+ in_progress: number_with_delimiter(ActiveJob.jobs.in_progress.count),
+ finished: number_with_delimiter(ActiveJob.jobs.finished.count)
+ }
+ },
+ status: :ok
+ end
+
+ private
+ def time_to_consult
+ params[:uptime].to_i || 5
+ end
+
+ def failed_execution
+ MissionControl::SolidQueueFailedExecution
+ end
+
+ def queue_job
+ MissionControl::SolidQueueJob
+ end
+end
diff --git a/app/controllers/mission_control/jobs/internal_api/navigation_controller.rb b/app/controllers/mission_control/jobs/internal_api/navigation_controller.rb
new file mode 100644
index 00000000..42b7e19f
--- /dev/null
+++ b/app/controllers/mission_control/jobs/internal_api/navigation_controller.rb
@@ -0,0 +1,7 @@
+class MissionControl::Jobs::InternalApi::NavigationController < MissionControl::Jobs::ApplicationController
+ def index
+ render partial: "layouts/mission_control/jobs/navigation_update", locals: {
+ section: params[:section].to_sym
+ }
+ end
+end
diff --git a/app/helpers/mission_control/jobs/navigation_helper.rb b/app/helpers/mission_control/jobs/navigation_helper.rb
index db2a48ed..65772e6e 100644
--- a/app/helpers/mission_control/jobs/navigation_helper.rb
+++ b/app/helpers/mission_control/jobs/navigation_helper.rb
@@ -2,7 +2,10 @@ module MissionControl::Jobs::NavigationHelper
attr_reader :page_title, :current_section
def navigation_sections
- { queues: [ "Queues", application_queues_path(@application) ] }.tap do |sections|
+ { dashboard: [ "Dashboard", application_dashboard_index_path(@application) ] }.tap do |sections|
+ sections[:queues] = [ "Queues", application_queues_path(@application) ]
+ sections[:queues] = [ "Queues", application_queues_path(@application) ]
+
supported_job_statuses.without(:pending).each do |status|
sections[navigation_section_for_status(status)] = [ "#{status.to_s.titleize} jobs (#{jobs_count_with_status(status)})", application_jobs_path(@application, status) ]
end
diff --git a/app/models/mission_control/solid_queue_failed_execution.rb b/app/models/mission_control/solid_queue_failed_execution.rb
new file mode 100644
index 00000000..039ae778
--- /dev/null
+++ b/app/models/mission_control/solid_queue_failed_execution.rb
@@ -0,0 +1,3 @@
+class MissionControl::SolidQueueFailedExecution < MissionControl::SolidQueueRecord
+ self.table_name = 'solid_queue_failed_executions'
+end
diff --git a/app/models/mission_control/solid_queue_job.rb b/app/models/mission_control/solid_queue_job.rb
new file mode 100644
index 00000000..4c9f92a4
--- /dev/null
+++ b/app/models/mission_control/solid_queue_job.rb
@@ -0,0 +1,6 @@
+class MissionControl::SolidQueueJob < MissionControl::SolidQueueRecord
+ self.table_name = 'solid_queue_jobs'
+
+ scope :pendings, -> { where(finished_at: nil) }
+ scope :finisheds, -> { where.not(finished_at: nil) }
+end
diff --git a/app/models/mission_control/solid_queue_record.rb b/app/models/mission_control/solid_queue_record.rb
new file mode 100644
index 00000000..1da46599
--- /dev/null
+++ b/app/models/mission_control/solid_queue_record.rb
@@ -0,0 +1,7 @@
+class MissionControl::SolidQueueRecord < ApplicationRecord
+ self.abstract_class = true
+
+ if !ActiveRecord::Base.connection.data_source_exists?('solid_queue_jobs')
+ connects_to database: { writing: :queue, reading: :queue }
+ end
+end
diff --git a/app/views/layouts/mission_control/jobs/_navigation.html.erb b/app/views/layouts/mission_control/jobs/_navigation.html.erb
index 67347fa9..2cc2df81 100644
--- a/app/views/layouts/mission_control/jobs/_navigation.html.erb
+++ b/app/views/layouts/mission_control/jobs/_navigation.html.erb
@@ -1,4 +1,4 @@
-
+
<% navigation_sections.each do |key, (label, url)| %>
- ">
@@ -7,3 +7,31 @@
<% end %>
+
+
diff --git a/app/views/layouts/mission_control/jobs/_navigation_update.html.erb b/app/views/layouts/mission_control/jobs/_navigation_update.html.erb
new file mode 100644
index 00000000..d9e8a74d
--- /dev/null
+++ b/app/views/layouts/mission_control/jobs/_navigation_update.html.erb
@@ -0,0 +1,7 @@
+
+ <% navigation_sections.each do |key, (label, url)| %>
+ - ">
+ <%= link_to label, url %>
+
+ <% end %>
+
diff --git a/app/views/layouts/mission_control/jobs/application.html.erb b/app/views/layouts/mission_control/jobs/application.html.erb
index 64c8dc5c..80dee801 100644
--- a/app/views/layouts/mission_control/jobs/application.html.erb
+++ b/app/views/layouts/mission_control/jobs/application.html.erb
@@ -8,6 +8,8 @@
+
+
<%= stylesheet_link_tag "mission_control/jobs/application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags "application", importmap: MissionControl::Jobs.importmap %>
diff --git a/app/views/mission_control/jobs/dashboard/index.html.erb b/app/views/mission_control/jobs/dashboard/index.html.erb
new file mode 100644
index 00000000..70101665
--- /dev/null
+++ b/app/views/mission_control/jobs/dashboard/index.html.erb
@@ -0,0 +1,182 @@
+<% navigation(title: "Dashboard", section: :dashboard) %>
+
+
+
+
+
+ General Overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/routes.rb b/config/routes.rb
index 96ba0bb5..cbdf8cda 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,5 +1,6 @@
MissionControl::Jobs::Engine.routes.draw do
resources :applications, only: [] do
+ resources :dashboard, only: [ :index ]
resources :queues, only: [ :index, :show ] do
scope module: :queues do
resource :pause, only: [ :create, :destroy ]
@@ -17,6 +18,11 @@
end
end
+ namespace :internal_api do
+ resources :dashboard, only: [ :index ]
+ resources :navigation, only: [ :index ]
+ end
+
resources :jobs, only: :index, path: ":status/jobs"
resources :workers, only: [ :index, :show ]
diff --git a/test/controllers/internal_api/dashboard_controller_test.rb b/test/controllers/internal_api/dashboard_controller_test.rb
new file mode 100644
index 00000000..794ba485
--- /dev/null
+++ b/test/controllers/internal_api/dashboard_controller_test.rb
@@ -0,0 +1,73 @@
+require "test_helper"
+
+class MissionControl::Jobs::InternalApi::DashboardControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @base_url = mission_control_jobs.application_internal_api_dashboard_index_url(@application)
+ MissionControl::SolidQueueJob.destroy_all
+ MissionControl::SolidQueueFailedExecution.destroy_all
+ end
+
+ test "index returns valid JSON structure" do
+ get @base_url
+ assert_response :ok
+
+ response_data = JSON.parse(response.body)
+ assert response_data.key?("uptime")
+ assert response_data.key?("total")
+
+ %w[label pending failed finished].each do |key|
+ assert response_data["uptime"].key?(key)
+ end
+
+ %w[failed pending scheduled in_progress finished].each do |key|
+ assert response_data["total"].key?(key)
+ end
+ end
+
+ test "index correctly formats numbers with delimiters" do
+ ActiveJob::Base.queue_adapter = :test
+
+ 1234.times { FailingJob.perform_later }
+ 1678.times { DummyJob.perform_later }
+
+ await_perform_all_enqueued_jobs
+
+ get @base_url
+ assert_response :ok
+
+ response_data = JSON.parse(response.body)
+
+ assert_equal "1,234", response_data["total"]["failed"]
+ assert_equal "1,678", response_data["total"]["finished"]
+ end
+
+ test "index calculates uptime data correctly" do
+ job = MissionControl::SolidQueueJob.create!(queue_name: 'default', class_name: 'DummyJob')
+ MissionControl::SolidQueueFailedExecution.create!(job_id: job.id, created_at: 2.seconds.ago)
+ MissionControl::SolidQueueJob.create!(queue_name: 'default', class_name: 'DummyJob', finished_at: 3.seconds.ago)
+
+ get @base_url, params: { uptime: 5 }
+ assert_response :ok
+
+ response_data = JSON.parse(response.body)
+ uptime = response_data["uptime"]
+
+ assert_equal 1, uptime["failed"]
+ assert_equal 1, uptime["finished"]
+ end
+
+ test "index handles custom uptime parameter" do
+ job = MissionControl::SolidQueueJob.create!(queue_name: 'default', class_name: 'DummyJob', finished_at: Time.now)
+ MissionControl::SolidQueueFailedExecution.create!(job_id: job.id, created_at: 10.seconds.ago)
+
+ get @base_url, params: { uptime: 15 }
+ assert_response :ok
+
+ response_data = JSON.parse(response.body)
+ uptime = response_data["uptime"]
+
+ assert_equal 0, uptime["pending"]
+ assert_equal 1, uptime["failed"]
+ assert_equal 1, uptime["finished"]
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index bb458fbd..7612931f 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -113,4 +113,15 @@ def schedule_recurring_tasks_async(wait: 1.second)
yield if block_given?
@scheduler.stop
end
+
+ def await_perform_all_enqueued_jobs
+ @worker.start
+
+ while ActiveJob.jobs.pending.count > 0 do
+ sleep(1.second)
+ end
+
+ yield if block_given?
+ @worker.stop
+ end
end