Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module MissionControl::Jobs::AdapterFeatures
extend ActiveSupport::Concern

included do
helper_method :supported_job_statuses, :queue_pausing_supported?, :workers_exposed?, :recurring_tasks_supported?
helper_method :supported_job_statuses, :queue_pausing_supported?, :workers_exposed?, :supports_dashboard?, :recurring_tasks_supported?
end

private
Expand All @@ -21,4 +21,8 @@ def workers_exposed?
def recurring_tasks_supported?
MissionControl::Jobs::Current.server.queue_adapter.supports_recurring_tasks?
end

def supports_dashboard?
MissionControl::Jobs::Current.server.queue_adapter.supports_dashboard?
end
end
4 changes: 4 additions & 0 deletions app/controllers/mission_control/jobs/dashboard_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class MissionControl::Jobs::DashboardController < MissionControl::Jobs::ApplicationController
def index
end
end
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class MissionControl::Jobs::InternalApi::NavigationController < MissionControl::Jobs::ApplicationController
include ActionView::Helpers::NumberHelper
include MissionControl::Jobs::NavigationHelper


def index
@navigation_sections = navigation_sections

render partial: "layouts/mission_control/jobs/navigation_update", locals: {
section: params[:section].to_sym
}
end
end
10 changes: 8 additions & 2 deletions app/helpers/mission_control/jobs/navigation_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ module MissionControl::Jobs::NavigationHelper
attr_reader :page_title, :current_section

def navigation_sections
{ queues: [ "Queues", application_queues_path(@application) ] }.tap do |sections|
sections = { }
sections[:dashboard] = [ "Dashboard", application_dashboard_index_path(@application) ] if supports_dashboard?

sections.tap do |sections|
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) ]
sections[navigation_section_for_status(status)] = [ "#{status.to_s.titleize} jobs (#{jobs_count_with_status(status)})", application_jobs_path(@application, status) ]
end

sections[:workers] = [ "Workers", application_workers_path(@application) ] if workers_exposed?

sections[:recurring_tasks] = [ "Recurring tasks", application_recurring_tasks_path(@application) ] if recurring_tasks_supported?
end
end
Expand Down
3 changes: 3 additions & 0 deletions app/models/mission_control/solid_queue_failed_execution.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class MissionControl::SolidQueueFailedExecution < MissionControl::SolidQueueRecord
self.table_name = 'solid_queue_failed_executions'
end
6 changes: 6 additions & 0 deletions app/models/mission_control/solid_queue_job.rb
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions app/models/mission_control/solid_queue_record.rb
Original file line number Diff line number Diff line change
@@ -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
51 changes: 50 additions & 1 deletion app/views/layouts/mission_control/jobs/_navigation.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="tabs is-boxed">
<div class="tabs is-boxed" id="navigation-sections">
<ul>
<% navigation_sections.each do |key, (label, url)| %>
<li class="<%= "is-active" if key == current_section %>">
Expand All @@ -7,3 +7,52 @@
<% end %>
</ul>
</div>

<script>
if (typeof navigationInterval === "undefined") {
var navigationInterval = null;
}

document.addEventListener("turbo:load", () => {
if (!window.Navigation || typeof window.Navigation.currentSection === 'undefined') {
window.Navigation = {
currentSection: "<%= @current_section %>",

changeSection(section) {
this.currentSection = section;
}
};

startNavigationInterval();
} else {
window.Navigation.currentSection = "<%= @current_section %>";
}
});

document.addEventListener("turbo:visit", () => {
clearInterval(navigationInterval);
startNavigationInterval();
});

function startNavigationInterval() {
let urlParams = new URLSearchParams(window.location.search);

navigationInterval = setInterval(() => {
urlParams = new URLSearchParams(window.location.search);
fetch(`<%= application_internal_api_navigation_index_path %>&server_id=${urlParams.get('server_id')}&section=${window.Navigation.currentSection}`)
.then(response => response.text())
.then(html => {
if (urlParams.get('server_id') !== 'solid_queue') {
console.log(urlParams.get('server_id'))
return;
}

const navigationSections = document.querySelector('#navigation-sections');
if (navigationSections) {
navigationSections.innerHTML = html;
}
})
.catch(error => console.error("Error fetching navigation update:", error));
}, 5000);
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<ul>
<% @navigation_sections.each do |key, (label, url)| %>
<li class="<%= "is-active" if key == section %>">
<%= link_to label, url %>
</li>
<% end %>
</ul>
20 changes: 10 additions & 10 deletions app/views/layouts/mission_control/jobs/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="turbo-cache-control" content="no-cache">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<%= stylesheet_link_tag "mission_control/jobs/application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags "application", importmap: MissionControl::Jobs.importmap %>
</head>
<body>

<section class="section">
<div class="container">
<%= render "layouts/mission_control/jobs/application_selection" %>
<%= render "layouts/mission_control/jobs/flash" %>
<%= render "layouts/mission_control/jobs/navigation" %>
<%= yield %>
</div>
</section>

<section class="section">
<div class="container">
<%= render "layouts/mission_control/jobs/application_selection" %>
<%= render "layouts/mission_control/jobs/flash" %>
<%= render "layouts/mission_control/jobs/navigation" %>
<%= yield %>
</div>
</section>
</body>
</html>
179 changes: 179 additions & 0 deletions app/views/mission_control/jobs/dashboard/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<% navigation(title: "Dashboard", section: :dashboard) %>

<div class="columns">
<div class="column">
<div class="notification">
<h6 class="title is-6">Pending</h6>
<span id="pending">--</span>
</div>
</div>
<div class="column">
<div class="notification">
<h6 class="title is-6">Scheduled</h6>
<span id="scheduled">--</span>
</div>
</div>
<div class="column">
<div class="notification">
<h6 class="title is-6">In Progress</h6>
<span id="in-progress">--</span>
</div>
</div>
<div class="column">
<div class="notification">
<h6 class="title is-6">Finished</h6>
<span id="finished">--</span>
</div>
</div>
<div class="column">
<div class="notification">
<h6 class="title is-6">Failed</h6>
<span id="failed">--</span>
</div>
</div>
</div>

<div class="columns">
<div class="column">
General Overview
</div>
<div class="column has-text-right">
<div class="select is-small">
<select id="change-uptime" value="5" onchange="handleSelectUptime(this.value)">
<option value="10">10 seconds</option>
<option value="5">5 seconds</option>
<option value="3">3 seconds</option>
<option value="1">1 second</option>
</select>
</div>
</div>
</div>
<hr/>

<div>
<canvas id="general-overview-chart"></canvas>
</div>

<script>

if (typeof uptimeInterval === "undefined") {
var uptimeInterval = null;
}
if (typeof chart === "undefined") {
var chart = null;
}

document.addEventListener("turbo:load", () => {
const canvas = document.getElementById('general-overview-chart');

if (!canvas)
return;

const ctx = canvas.getContext('2d');

if (chart) {
chart.destroy();
chart = null;
}

const finished = document.getElementById('finished');
const scheduled = document.getElementById('scheduled');
const pending = document.getElementById('pending');
const inProgress = document.getElementById('in-progress');
const failed = document.getElementById('failed');

let uptime = 5;

const labels = [];
const data = {
labels: labels,
datasets: [{
label: 'Success',
data: [],
fill: false,
borderColor: 'rgb(0, 219, 124)',
tension: 0.1
},
{
label: 'Error',
data: [],
fill: false,
borderColor: 'rgb(226, 15, 15)',
tension: 0.1
},
{
label: 'Pending',
data: [],
fill: false,
borderColor: 'rgb(237, 209, 0)',
tension: 0.1
}]
};

const config = {
type: 'line',
data: data,
};

chart = new Chart(ctx, config);

function handleSelectUptime(value) {
uptime = value;
clearInterval(uptimeInterval);
uptimeInterval = setInterval(() => updateChartData(), value * 1000);
}

async function updateChartData() {
try {
const response = await fetch(`<%= application_internal_api_dashboard_index_path %>&uptime=${uptime}`);
if (!response.ok) throw new Error('Network response was not ok');

const data = await response.json();

if (chart == null) return;

chart.data.labels.push(data.uptime.label);
chart.data.labels = chart.data.labels.slice(-20);

AddChartData(0, data.uptime.finished);
AddChartData(1, data.uptime.failed);
AddChartData(2, data.uptime.pending);

finished.innerHTML = data.total.finished;
inProgress.innerHTML = data.total.in_progress;
pending.innerHTML = data.total.pending;
scheduled.innerHTML = data.total.scheduled;
failed.innerHTML = data.total.failed;

chart.update();
} catch (error) {
console.error('Error at consult chart API:', error);
}
}

function AddChartData(datasetIndex, quantity) {
chart.data.datasets[datasetIndex].data.push(quantity);
chart.data.datasets[datasetIndex].data = chart.data.datasets[datasetIndex].data.slice(-10);
}

if (uptimeInterval != null)
clearInterval(uptimeInterval);

uptimeInterval = setInterval(() => updateChartData(), 5000);
updateChartData();

window.handleSelectUptime = handleSelectUptime;
});

document.addEventListener("turbo:before-render", () => {
if (uptimeInterval != null) {
clearInterval(uptimeInterval);
uptimeInterval = null;
}

if (chart) {
chart.destroy();
chart = null;
}
});
</script>
Loading